Testes unitários: um olhar para a qualidade de nossos testes

Lemuel Roberto
Engenharia Arquivei
5 min readMay 2, 2019

No início de minha jornada escrevendo testes unitários, aprendi sobre um perigoso modelo de programação: o CDD — Color Driven Development. Depois de estudar referências na literatura e escrever milhares de testes, está mais claro como escrever testes que oferecem a tão sonhada paz de espírito, que aumentam a produtividade do time ao longo do ciclo de vida do produto e que como bônus nos ajudam escrever código mais limpo. Neste artigo compartilhamos algumas dicas aplicáveis no dia a dia para melhorar a qualidade dos testes que você escreve.

Código vs Teste: o que queremos garantir

De forma geral, dada uma entrada, o código (i.e. função) computa e retorna um resultado. Um teste com qualidade garante que o código se comporta e retorna o resultado esperado em, no mínimo, todos os cenários descritos. Neste contexto, os testes unitários são os primeiros clientes/usuários de nosso código.

Os clientes, em sua maioria, não se importam com o como o código chega ao resultado esperado. O cliente, ou teste, pode esperar que algumas restrições como tempo de execução, processamento e uso de memória sejam respeitadas e este assunto merece seu próprio espaço de discussão. Grosso modo, nossos testes validam mais o que foi retornado do que como foi processado.

Considerando as principais atividades que impactam a base de código, as mais relevantes para as regras de negócio são, sem definição de importância, adição de novas funcionalidades, correção de bugs, modificação de funcionalidades e refatorações. Com exceção das refatorações, todas as outras atividades implicam em testes quebrados, pois por definição introduzem novo comportamento à aplicação. O principal desdobramento dessas implicações é que ganhamos confiança e agilidade para manter o código por todo o ciclo de vida do produto.

Manter um produto de sucesso, muitas vezes com centenas de milhares de linhas de código, representa a maior parte do tempo, esforço e custo gasto em seu ciclo de vida. Nesse meio tempo, melhorias de performance, legibilidade e até mudanças de serviços e bancos de dados podem ser implementadas com a segurança de que as regras de negócio do produto estão garantidas pelos testes unitários de qualidade. Dentro do Arquivei aproveitamos dessa tranquilidade para, por exemplo, migrar nossa arquitetura para soluções mais escaláveis.

Teste é código: zero código, zero bugs

Um fato importante é que testes automatizados também são código, portanto são naturalmente vulneráveis à falhas humanas e carecem de manutenção. Além disso, escrever, manter e executar testes custam tempo e isso deve deve ser considerado no planejamento. Por este motivo, se inspire, sempre que possível, no projeto no code para criar código e teste, se questionando como escrever menos para facilitar o trabalho do time e do seu próprio eu do futuro.

Brincadeiras a parte, aproveite todas as ferramentas ao seu dispor para que seja possível escrever menos testes, e testes que executam mais rápido e ainda assim validarão todos os fluxos necessários. Utilize o poder da linguagem e do compilador para entregar mais garantias a você: se apoie nas garantias de tipagem. Utilize ferramentas de análise estática de código em linguagens interpretadas para cobrir, com o mínimo de garantias, as partes da base de código sem testes automatizados. Entenda com profundidade quais são as facilidades oferecidas pela ferramenta de testes automáticos de sua escolha.

Exemplo: show me the code

Para entender na prática o que discutimos nas seções anteriores, o estudo de caso escolhido é composto por duas classes: Entity e Presenter. Entity representa uma entidade de nossa regra de negócio e Presenter é responsável por apresentar a entidade numa estrutura e formato convenientes. Os testes unitários foram propositalmente escritos com algumas deficiências que serão discutidas e corrigidas em detalhes. Os exemplos foram escritos em PHP com PHPUnit mas isso é irrelevante para o entendimento dos conceitos.

Exemplo de código com uma entidade e um apresentador.

Testes-

Nesta seção, discutiremos testes unitários que, embora cubram 100% das linhas de código, possuem falhas conceituais com implicações em sua assertividade. Discutiremos em detalhes as implicações dessas falhas e posteriormente apresentaremos exemplos de como melhorar a qualidade de suas validações. Para começarmos com esse exemplo do que não fazer, temos as classes EntityTest e PresenterTest que implementam 3 testes.

Exemplo de testes unitários com problemas conceituais.

Resumidamente, esses testes sofrem 2 grandes problemas:

  1. não validam o comportamento esperado pelo cliente;
  2. testam apenas os cenários mais triviais.

O teste PresenterTest::testPresent mostra apenas que o método Presenter::present é público e executável sem parâmetros.

Quando o assunto é garantir um contrato com o cliente, o teste PresenterTest::testToArray falha por não validar nem a estrutura nem o formato dos dados retornados.

O teste EntityTest::testGetId valida apenas o cenário mais trivial, tanto é que a classe Entity nem precisa de construtor.

É importante notar o uso incorreto de Mock e Stub onde poderíamos, e deveríamos, aproveitar os objetos concretos da classe Entity. Explore a documentação da PHPUnint para entender melhor o uso de Test Doubles pois essas técnicas tem seu valor e espaço no contexto correto.

O Stub utilizado em PresenterTest::testToArray empobrece a qualidade do teste. Poderíamos utilizar objetos Entity com mais cenários de teste.

Se removemos o Mock $entityMock->expects($this->exactly(1))->method('getId');, percebemos que as classes continuam com 100% de cobertura, mostrando o quão frágil e "Orientado a Cor" o teste é.

Exemplo de teste unitário arriscado (sem assert).

Perceba que a própria PHPUnit alerta sobre os testes arriscados (e.g. inúteis):

Resultado da execução dos testes.

Outra forma de expor as falhas desses testes é adicionar um novo campo no retorno do Presenter. Por exemplo a adição do campo name não quebra os testes, mas deveria.

Exemplo de alteração em código que não é detectada pelos testes, mas deveria.

Testes+

Nesta seção exploramos algumas técnicas para melhorar a qualidade de nossos testes com exemplos de como validar a API exposta por nosso código de forma efetiva. Para tal, utilizamos asserções mais adequadas, adicionamos mais cenários de testes e deixamos de utilizar Test Doubles de forma desnecessária.

É crucial, portanto, validar e garantir que os valores retornados são os esperados em seus mais diferentes níveis. Por isso é importante entender as características da linguagem e da ferramenta de testes que trabalhamos. Para mais detalhes das opções disponíveis na PHPUint, veja a documentação.

Para testarmos mais cenários de testes, utilizamos provedores de dados (i.e. dataProvider), uma importante ferramenta para implementar testes que cubram uma variedade mais ampla de entradas. Durante a investigação de uma falha de código (bug), os provedores são muito úteis para expor o cenário que gera o comportamento indesejado. A documentação da PHPUnit oferece mais detalhes sobre seu funcionamento e casos de uso.

Um detalhe, que vale o comentário, é o uso desnecessário do nullable type hint, ou até errada do ponto de vista das regras de negócio, mas que pode acabar surgindo no código para trapacear os testes com Stub que vão retornar null por padrão. Por esse motivo removemos a possibilidade de retorno null em Entity::getId.

Exemplo de melhorias no código e testes unitários.

Considerações finais

Em resumo, tão importante quanto escrever testes automatizados, é garantir que os testes nos ajudam a escrever códigos melhores e dão paz de espírito para o time manter e adicionar funcionalidades relevantes para os clientes de maneira ágil e consistente ao longo do ciclo de vida do produto. Explorar, conhecer e utilizar as ferramentas à disposição de maneira adequada é só um caminho mais fácil para criar e manter nossos produtos e bibliotecas.

--

--