Nesta postagem vou contar dois causos envolvendo testes e cache em software. Qualquer engenheiro de software em um projeto minimamente organizado, conhece a importância dos testes. O cache, também é importante em determinados contextos: se obter uma informação tem um custo maior que mantê-la em cache, vale considerar colocar ela em um cache. Algumas práticas para cache devem ser consideradas, como ter um tempo de vida e ser transparênte para a grante maioria da aplicação.

Falando em testes unitários em software, eles devem seguir algumas boas práticas para que sejam efetivos, fáceis de entender, alterar e manter.

Em primeiro lugar, testes devem ter foco. É basicamente por isso que são chamados de “unitários”: cada teste deve validar uma unidade específica da lógica do software. Por exemplo, se o código possui uma instrução if, deve existir um teste para avaliar o caso em que a condição é verdadeira e outro para o caso em que a condição é falsa — sempre de forma independente.

Além disso, testes precisam ser determinísticos. Isso pode parecer óbvio, mas quando lidam com informações de data, hora ou valores randômicos, garantir determinismo pode se tornar mais complicado.

Outro ponto essencial é que testes precisam ser rápidos. Na maioria dos casos, quanto mais testes houver, melhor. Porém, a execução não pode se tornar um gargalo. Algumas práticas de desenvolvimento rodam testes constantemente em background, à medida que o código de negócio é alterado. Outras adotam o Test-Driven Development (TDD), no qual o desenvolvimento começa pelo teste (que inicialmente falha, já que não há lógica implementada), e então o código de negócio é escrito até que o teste seja bem-sucedido.

Também é fundamental que os testes sejam isolados entre si. Um teste não pode depender de outro. Ele deve ser capaz de montar seu próprio cenário de dados e não deve assumir que informações criadas ou alteradas em testes anteriores estarão disponíveis ou em um estado adequado.

Recentemente comentei com um amigo — também engenheiro de software — sobre uma situação em que quebrei um sistema em produção. Não foi uma falha catastrófica, mas trouxe um enorme prejuízo de qualidade, considerando que a alteração no código foi mínima. Eu havia adicionado um campo em uma classe e escrito uma lógica que utilizava esse novo campo. Escrevi testes, publiquei nos ambientes de teste na nuvem, executei verificações adicionais e, aparentemente, tudo funcionava. Porém, ao enviar para produção, tudo parou.

O único campo que adicionei na classe fez com que os objetos dessa classe não conseguissem mais sair do cache e voltar para a aplicação, tornando-os incompatíveis. Como várias funcionalidades dependiam dessa classe, o impacto foi considerável. Felizmente, bastou invalidar todo o cache para que a aplicação voltasse a funcionar. No entanto, nenhum dos milhares de testes existentes foi capaz de detectar esse problema antes que ele estourasse em produção.

Meu amigo não me deixou sozinho no vexame e compartilhou também um caso parecido, envolvendo testes e cache — mas que, felizmente, não chegou a quebrar a produção. Uma nova feature havia sido desenvolvida e precisava de cobertura de testes. O teste criava cenários no banco de dados, executava verificações, mas falhava sem motivo aparente. Curiosamente, a feature funcionava tanto localmente quanto nos ambientes de homologação usados pelos clientes.

Apesar da estranheza, havia alguns indícios. Quando o teste rodava isoladamente, funcionava (um clássico). Mas quando todos os testes do mesmo arquivo eram executados, alguns falhavam. O que poderia estar acontecendo?

Durante o teste, era necessário criar um token JWT para acessar a API. Esse token, no entanto, era armazenado em um cache construído pelos próprios métodos utilitários dos testes. O cache era simples: um Map cuja chave era o e-mail do usuário de teste. Existiam diferentes usuários, cada um com permissões específicas para diferentes cenários. Essa abordagem economizava alguns bons segundos quando a taberia inteira de testes rodava.

Para facilitar, havia um mecanismo que criava automaticamente o usuário de teste. Para garantir isolamento, outro mecanismo limpava absolutamente todas as tabelas do banco de dados entre execuções, obrigando cada teste a construir seu próprio cenário do zero. Porém, o token continuava armazenado no cache. Assim, o novo teste até criava corretamente o usuário no banco, mas, ao buscar o token, recebia a versão armazenada no cache.

Esse detalhe passou despercebido por cerca de oito meses — período em que o sistema recebeu muitas novas funcionalidades e dezenas de testes semelhantes foram escritos — sem nunca apresentar falhas relacionadas a esse cache. Até que finalmente o problema apareceu.

Desenvolvimento de software é uma atividade traiçoeira. Um sistema sem bugs é apenas um sistema que não foi usado o suficiente.

Categorized in:

Uncategorized,

Last Update: 15/09/2025