IT & Engineering
Bugs de software e como corrigi-los mais rápido
O custo de correção de bugs não é igual para todas as empresas. O custo não depende apenas das taxas de operação e serviço, mas também de quanta dívida técnica você tem.
Quando falamos de dívida técnica, estamos falando do custo gerado quando as empresas não resolvem problemas que as afetarão no futuro. Isso pode sair caro, mas e se existirem padrões ou estratégias que a equipe de desenvolvimento possa aproveitar para reduzir o tempo necessário para identificar e resolver bugs?
Neste artigo, vamos provar o que dizemos e compartilhar uma estratégia que consideramos ser a mais eficaz para reduzir o tempo de resolução de bugs.
O que é um bug de software?
Um bug de software é apenas um erro de programação em um sistema. Bugs são bastante comuns em um único programa ou aplicativo, mas se ampliarmos a visão para uma plataforma inteira de serviços integrados e códigos interconectados, a perspectiva de procurar bugs começa a parecer com aquele velho ditado sobre agulhas e palheiros.
Corrigir um bug leva mais tempo do que escrever uma linha de código. Por isso, faz sentido investir no desenvolvimento do seu software desde o início. Certo?
O custo do sucesso: processos de desenvolvimento de software
Preparar-se para o sucesso tem um alto custo inicial, tanto em termos de tempo quanto de recursos. O desenvolvimento de software sob medida pode ter um preço médio de até US$ 250.000 dependendo da sua infraestrutura e necessidades, portanto, é fundamental para os custos criar um plano de estratégia de manutenção em vez de arcar com o custo da criação constante de novos códigos.
Modelando o seu ambiente de produção
Você tem a mesma chance de reproduzir um bug em um ambiente local ou controlado que um rato tem de sobreviver em uma fazenda de gatos — a menos que você tenha modelado o seu ambiente de produção da forma mais fiel possível.
Essa solução pode assumir muitas formas, incluindo a prática comum de criar um ambiente de homologação ou desenvolvimento. Mas isso sai caro, e ambientes de longa duração tendem a se desviar do padrão ao longo do tempo. Nos 20 anos de experiência de Derrick como profissional de desenvolvimento, nada chega perto da capacidade de reproduzir um problema na sua máquina local. Isso significa que você precisa ter a capacidade de modelar o ambiente de produção localmente da maneira mais realista e reprodutível possível.
As implementações mais bem-sucedidas disso que já vimos estão em arquiteturas que usam microsserviços ou, como gostamos de chamá-los, “serviços de escopo de domínio”. Nesses ambientes, o objetivo é levantar cada serviço dependente de modo que o serviço no qual você está trabalhando não perceba a diferença entre ser executado na sua máquina local e em produção.
Como você pode fazer isso acontecer? A maneira mais fácil é exigir que as pessoas responsáveis pelos serviços forneçam uma implementação falsa ou simulada, e fácil de usar, da interface pública do serviço. Esse é um exemplo perfeito de se preparar para o sucesso. Criar essas simulações exige tempo e recursos. Se a cultura da sua equipe de desenvolvimento for a de fornecer essas ferramentas, o objetivo de modelar o seu ambiente de produção em código se torna muito mais fácil, pois grande parte do trabalho duro de implementar um serviço falso ou simulado é feito por você. Caso contrário, você terá que pagar o preço de criá-lo por conta própria.
Ao simular os seus serviços, você não pode apenas recriar interfaces externas, também precisa incluir os protocolos de transporte. Você deve estar se perguntando: por quê? Não seria melhor simplesmente evitar fazer chamadas remotas de alto custo? Seria mais barato? Sim, mas não seria eficaz. O objetivo é modelar o ambiente de produção da forma mais fiel possível; isso inclui o transporte que o seu aplicativo usa para se comunicar com outros serviços. Seja HTTP, GRPC ou o simples TCP, pequenas variações na pilha de transporte ou nas bibliotecas de conversão (marshaling) podem ter impactos muito sutis na forma como o seu código opera em produção.
Hora da história: como aprendemos isso no Mailgun
Tínhamos um serviço que trocou as bibliotecas de marshalling JSON num esforço para aumentar o desempenho, mas isso introduziu involuntariamente um problema de análise (parsing) de Unicode. Simular a interface em vez de realmente chamar o transporte e a função de marshalling teria ocultado esse problema de nós.
Outro problema inesperado envolveu uma alteração na biblioteca DNS do Golang, que teria destruído completamente o nosso ambiente de produção se não fosse pela nossa suíte funcional — e sim, executamos uma implementação de simulação de DNS para os nossos testes.
Criação de testes funcionais para encontrar casos extremos e diagnosticar problemas
Certo, então, depois de modelar o nosso ambiente, a primeira coisa que você precisa fazer é criar uma suíte de testes funcionais. Uma suíte de testes funcionais é um contêiner que abriga um conjunto de testes criados para auxiliar na execução e no relatório dos status de execução de testes. Você pode adicionar casos de teste e planos às suas suítes para abranger uma variedade de cenários e casos extremos. Quanto melhor for a sua suíte, mais fácil será tentar diagnosticar um problema.
Se o problema que você está tentando resolver existir como um teste funcional, será fácil (e rápido) substituir os valores em teste pelos valores exatos encontrados em produção. Apenas reproduzir o problema localmente recriando os dados exatos da produção pode levar à solução. Se você facilitou a forma de importar dados da produção ou simular dados da produção, isso pode ser uma ferramenta inestimável para você.
Se você não tiver um teste funcional existente cobrindo o cenário (o que é comum, já que a sua base de usuários frequentemente encontra formas irritantes, digo… únicas e imprevistas de usar o seu sistema), você terá que criá-lo.

Parte de nos prepararmos para o sucesso envolve conduzir testes funcionais ou ter a capacidade de criar rapidamente um teste funcional. Os testes de unidade (que testam a menor parte do código que pode ser isolada logicamente em um sistema) podem ser úteis depois que você tiver delimitado o problema. Um teste funcional permite que você trace linhas gerais pelo seu serviço e entenda, da perspectiva do cliente, como o serviço opera em um determinado cenário. Ao depurar, testar o produto é sempre mais importante do que testar o código.
Por que você deve evitar testes manuais e o que fazer no lugar deles
O oposto do teste funcional é o teste manual. O teste manual é um tipo de teste de software no qual os casos de teste são executados manualmente por profissionais de testes, sem o uso de qualquer ferramenta automatizada. Você deve sempre priorizar o teste funcional em relação ao teste manual, já que o teste manual é propenso a erros e não é reprodutível.
Muitos profissionais de programação cometem o erro de rodar um serviço localmente e testar os endpoints manualmente para diagnosticar problemas. Tendo feito isso por muitos anos, mal podemos dizer em primeira mão quantas vezes reproduzimos um problema sem saber exatamente como fizemos isso, o que nos levou a perder o controle (e a revirar mesas) tentando refazer nossos passos.
As sessões de depuração muitas vezes passam de horas para dias e, com os testes manuais, todos os cenários que tentamos ficam sem documentação e se misturam. Porém, se todos esses cenários tentados forem escritos como testes funcionais, cada cenário poderá se tornar parte do histórico da sessão de depuração.
Existe um segredo para os testes funcionais. Escrever testes funcionais para diagnosticar problemas é uma estratégia fundamental para minimizar o tempo de resolução de bugs. Contudo, esse método de diagnóstico não funciona se for difícil escrever o teste funcional. A solução? Simplifique as coisas desde o primeiro passo.
Crie uma suíte de funções auxiliares e asserções simples. O objetivo é tornar ações como loops de repetição (retry) e importar suítes de dados MUITO fáceis de fazer. Essas funções auxiliares devem estar entre as primeiras ferramentas criadas na escrita inicial do software para dar suporte a novos testes funcionais no futuro.
Um exemplo: como fizemos isso no Mailgun
A API principal /messages no Mailgun é operada por um serviço que chamamos de influx. Esse serviço ingere tanto mensagens HTTP quanto SMTP em nosso sistema e se comunica com praticamente todos os outros serviços na nossa suíte de serviços de envio de mensagens.
Como resultado, investimos muito na nossa suíte de testes funcionais e modelamos o nosso ambiente de produção em código para essa parte muito pública do nosso catálogo de serviços. Criamos mais de 700 testes, a maioria deles funcionais, que são concluídos em cerca de seis minutos em uma máquina local.
Muitos desses testes foram incluídos durante sessões de depuração ou diagnóstico e, em seguida, adicionados à nossa suíte de testes. Se for necessário um novo cenário, ele é adicionado à suíte funcional e forma uma proteção contra regressões futuras.
Considerações finais
O verdadeiro custo do desenvolvimento de software está na manutenção e no diagnóstico do código. É por isso que se preparar para o sucesso, criando uma suíte de ferramentas que possa ser usada para diminuir o tempo de resolução, vale o tempo e o dinheiro extras.
Esse investimento inicial continuará a render frutos à medida que o projeto prosseguir.
Gostou da nossa visão sobre como encontrar e diagnosticar bugs de software? Se quiser mais conteúdos como esse, não se esqueça de assinar a nossa newsletter.