Ode to Testes Unitários

Kirill Blocked Unblock Seguir Seguindo 12 de janeiro

A lei do teste: O grau em que você sabe como o seu software se comporta é o grau em que você o testou com precisão.
– “Code Simplicity”

A fim de verificar a exatidão de uma aplicação, usamos diferentes tipos de testes: alguns verificam a lógica de uma pequena função ou classe, outros verificam todas as camadas do sistema da interface do usuário para bancos de dados e serviços externos. Alguns tipos de testes podem estar em algum lugar entre chamar a API do aplicativo e usar stubs para serviços externos. O número de elementos testados define o escopo do teste: menos elementos – escopo menor, mais elementos – escopo maior. Quanto maior o escopo dos testes, mais recursos computacionais são necessários e mais tempo é necessário para executar os testes. Além disso, testes com um escopo maior são mais difíceis de manter e foram feitos para um ciclo de feedback mais longo.

Hoje vamos falar sobre Testes Unitários, que são colocados na parte inferior da pirâmide de testes e têm o ciclo de feedback mais curto.

Testes Unitários

Twitter @ismonkeyuser

Então, o que é um teste de unidade? É um código que pode verificar se outro código funciona conforme o esperado. Uma das qualidades importantes de um Teste de Unidade é o seguinte:

  • testa a funcionalidade dos elementos da aplicação – unidades – classes e funções;
  • é escrito por desenvolvedores enquanto trabalha no código;
  • é fácil de executar sem ter que configurar um ambiente adicional;
  • requer pouco tempo para ser executado;
  • Ele pode ser facilmente integrado ao CI porque não requer ambiente adicional.

Os testes de boa unidade seguem cinco regras, que formam a sigla PRIMEIRA.

Rápido – testes deve ser rápido. Quando os testes são executados lentamente, você não deseja executá-los com frequência. E se você não os executar com frequência, não encontrará problemas com antecedência suficiente para corrigi-los facilmente. Você não se sentirá tão livre para limpar o código.

Independente (ou isolado) – os testes não devem depender um do outro. O desenvolvedor deve ser capaz de executar testes na ordem que desejar (mesmo em paralelo). Quando os testes dependem uns dos outros, o primeiro a falhar provoca uma cascata de falhas a jusante, dificultando o diagnóstico e ocultando defeitos a jusante.

O Princípio de Responsabilidade Única (SRP) dos princípios do SOLID diz que as classes devem ser pequenas e de propósito único. Isso pode ser aplicado aos seus testes também. Se um dos seus métodos de teste puder quebrar por mais de um motivo, considere dividi-lo em testes separados.

Repetitivo – os testes devem ser reproduzíveis em qualquer ambiente: no ambiente de produção, no ambiente de controle de qualidade e até mesmo no laptop do desenvolvedor. Os resultados dos testes devem ser os mesmos todas as vezes e em todos os locais.

Auto-validação – cada teste deve ser capaz de determinar se a saída é esperada ou não. Eles passam ou falham. Você não deve ter que ler um arquivo de log ou comparar arquivos diferentes para ver se os testes são aprovados. Se os testes não forem auto-validados, a falha pode se tornar subjetiva e a execução dos testes pode exigir uma avaliação manual longa.

Completo / Atempado – os testes devem ser escritos no tempo apropriado, com a implementação dos recursos. O teste pós-fato exige que os desenvolvedores refatrem o código de trabalho e façam um esforço adicional para que os testes cumpram esses PRIMEIROS princípios.

Por que testar?

Assim que sabemos o que é testado, a próxima pergunta é – por quê? A resposta simples é porque os Testes de Unidade nos ajudam a testar nosso código e encontrar defeitos. Isso é verdade, mas há uma razão menos óbvia e mais séria: eles ajudam a manter uma aplicação implementável e sustentável – adicionar novos recursos e corrigir erros mais rapidamente.

A manutenção é frequentemente descrita como a capacidade de entender, alterar e testar um aplicativo facilmente. Os Testes Unitários contribuem para todos os três:

  • Quando Testes Unitários são uma implementação de cenários de teste, eles ajudam a entender a funcionalidade do componente;
  • Testes de unidade podem dizer rapidamente se a alteração quebra a funcionalidade existente;
  • O desenvolvimento com Testes Unitários força o desenvolvedor a projetar componentes mais testáveis (muitas vezes significa um código melhor).

O argumento mais comum contra os Testes Unitários é que eles aumentam o tempo de desenvolvimento. Isso é verdade para o início de um projeto, quando apenas o conjunto inicial de recursos foi implementado e quase todos os códigos são recém-escritos. Mas, à medida que o projeto avança, você se verá reescrevendo o código antigo com mais e mais frequência. Você começará a gastar tempo tentando entender como o código antigo funciona e garantindo que sua alteração não o tenha quebrado.

É exatamente onde os Testes de Unidade podem ajudá-lo. Sim, pular Testes de Unidade permite que você se desenvolva mais rapidamente no começo, mas se o aplicativo for usado, por exemplo, por vários anos, introduzindo mais recursos ao longo do caminho, você deve manter o aplicativo sustentável e escrever Testes de Unidade.

Cobertura de código

A cobertura de código é uma medida usada para descrever o grau em que o código-fonte de um programa é testado por um determinado conjunto de testes. Normalmente, um programa com alta cobertura de código foi testado mais profundamente e tem uma chance menor de conter erros do que um programa com baixa cobertura de código. Mas apenas normalmente, porque mesmo que 100% do código seja coberto, ainda não sabemos nada sobre a qualidade dos testes. Alguns testes podem cobrir o código, mas são inúteis, sem asserções necessárias ou simplesmente sendo mal projetados. Parece contra-intuitivo, mas saber qual código é descoberto é mais importante. Código descoberto significa que não há cenários de Teste Unitário para verificar se o código funciona conforme o esperado.

Isso torna a cobertura de código mais uma ferramenta de desenvolvedor para entender que são necessários mais cenários de teste do que uma ferramenta a ser usada como um gate de qualidade de integração contínua. Ainda é uma boa ideia concordar com um nível mínimo de cobertura de código para identificar pontos de acesso, mas você não pode confiar cegamente na cobertura de código alto e devemos nos preocupar com a qualidade dos Testes de Unidade.

TDD

Processo TDD: teste vermelho à direita -> torná-lo verde -> fazê-lo funcionar

Vamos falar sobre o processo de TDD. Em um desenvolvimento orientado a testes, cada novo recurso começa com a criação de um teste para ele. Para escrever um teste, o desenvolvedor deve entender claramente as especificações e os requisitos do recurso. Esta é uma característica diferenciadora do desenvolvimento orientado a testes versus a escrita de Testes Unitários após o código: Faz com que o desenvolvedor se concentre nos cenários de teste antes de escrever o código – uma diferença sutil, mas importante.

Na segunda etapa, executamos todos os testes e verificamos se o novo teste falha. Então, nós escrevemos o código.

O próximo passo é escrever algum código que resulte na passagem do teste. O novo código escrito neste estágio não é perfeito e pode, por exemplo, passar no teste de maneira deselegante.

Neste ponto, o único propósito do código escrito é passar no teste. O programador não deve escrever código que esteja além da funcionalidade que o teste verifica.

Em seguida, executamos testes.

Se todos os casos de teste passarem, o programador pode ter certeza de que o novo código atende aos requisitos de teste e não quebra nem prejudica os recursos existentes.

Se eles não passarem, o novo código deve ser ajustado até que eles aconteçam.

Para o passo depois disso, refatoramos o código.

Se necessário, repetimos esse processo, começando com outro novo teste, e o ciclo é então repetido para avançar a funcionalidade.
A abordagem TDD tem algumas limitações, por exemplo, é difícil aplicá-la a grandes projetos de dados e ciência de dados, por causa da parte desses aplicativos baseada em dados, também é difícil aplicar a GUI ou o desenvolvimento frontend. Mas esse é o tema de outro post.

Conclusão

Assim, concluindo, o Teste de Unidade é uma técnica poderosa que permite verificar rapidamente o código por meio de cenários de teste, acelerar o desenvolvimento a longo prazo e manter o código em um estado de manutenção.