Testar é um passo obvio na escrita de código. Sempre que terminamos de escrever (ou enquanto ainda estamos escrevemos), executamos o código para verificar se o resultado esperado está sendo alcançado. Afinal, um código que não executa ou não gera o resultado, não serve pra nada.
Acredito que a maioria, começa simplesmente executando o código (abrindo a página no browser, compilando e executando pela IDE ou símplesmente fazendo uma chamada via cli) e verificando manualmente o resultado. Quando o resultado não corresponde, adicionamos mais informação, via log, ferramentas de debug ou uma exibição de informações própria da linguagem (como var_dump
em PHP) e procuramos o problema, ou onde o que deveria ter sido feito não aconteceu.
Como todo bom preguiçoso, clicar em uma pagina ou IDE começa a ficar chato depois de um tempo, principalmente quando se necessita de interação para chegar onde se quer testar. O que acontece quando seu software cresce? Quando seu código não é mais um script de 300 linhas e sim um emaranhado de classes e chamadas entre elas?
Testando código com código
Para facilitar os testes durante a escrita de código, podemos utilizar código para testar código, ou seja, executamos o nosso código, capturamos o resultado e validamos, utilizando tambem código. Um exemplo bastante simples em PHP:
<?php function soma($num1, $num2) { return $num1 + $num2; } $resultado = soma(1, 1); if ($resultado != 2) { die("Resultado da soma errado"); }
Por que fazemos isso? Alguns dos meus motivos são:
- É mais facil retestar após algumas alterações no código;
- É mais facil testar quando o resultado não é algo trivial como um inteiro ou booleano;
Conforme o código cresce, uma opção é separar os testes, e testar partes menores por vez, e não somente o todo de uma unica vez.
Testes unitários
Um dos tipos de teste é o teste unitário, ou seja, um teste de unidade. Podemos definir uma unidade como o menor pedaço de código que podemos testar. A unidade mais utilizada para OOP são metodos de classe.
A idéia é uma checar pedaços pequenos de código, o que pode facilitar o debug, acelerar a execução do seu teste (quando a execução do código completo é relativamente lenta e você precisa testar apenas uma chamada, por exemplo) e isolar o código, evitando que um erro inserido em um determinado código seja interpretado como um erro em outro lugar (se a função X está retornando um valor incorreto mas tudo o que você percebe é um comportamento inesperado na função Z).
Outro benefício é que podemos aumentar as opções de teste, exercitando nosso código de diversas outras maneiras, e não só mais o “caminho feliz”. Podemos verificar se nosso código realmente filtra as entradas, valida os valores e executa o comportamento condicional correto. O que é um benefício, também pode ser tornar um malefício quando o nosso código só falha quando não isolado, ou seja, quando chamado em conjunto com outro pedaço de código. Ai entram os testes de integração.
Testes de integração
Testes de integração são exatamente o oposto de testes unitários. O objetivo é garantir que o resultado é atingido quando todo ou parte do código interage, e é utilizado em conjunto. Porém, os limites não são necessáriamente definidos. Podemos testar a integração entre duas funções, quatro metódos, seis classes, ou mais de um recurso (banco de dados, API, etc.).
Qual é melhor?
Aquele que é feito. Ambos tem seu valor e são aplicados por razões diferentes mas um não substitui o outro. Os testes unitários geram grande confiabilidade no código escrito, porém, não tem como garantir o mesmo resultado quando outra parte do código muda. Principalmente quando essa outra parte não está sob o controle do desenvolvedor. Os códigos de integração podem verificar essa interação, porém, podem se tornar muito lentos em diversos casos.
Outros tipos de teste
Os testes unitários e de integração não são os únicos tipos de teste disponiveis. Existem diversos outros, lembrando que os testes que escrevemos, muito provavelmente, acabam sendo de mais de um tipo por vez. Alguns tipos são:
- End-to-end: Sãos os tipos de teste em que é feita a verificação do processo como um todo, normalmente simulando um usuário;
- White-box: Testes onde o código de teste conhece a implementação do código sendo testado. Um exemplo é quando estamos testando se um determinado código interage com o banco de dados, então verificamos se a chamada para o banco de dados foi feita;
- Black-box: Testes onde o código não conhece a implementação. Utilizando o mesmo exemplo, verificamos se a interação com o banco de dados foi feita, verificando o próprio banco de dados. No caso de um insert, procurariamos pelo registro inserido;
- Smoke test: Testes rápidos, e muitas vezes rasos. O objetivo é apenas garantir um mínimo de confiança. Normalmente testamos apenas o “caminho feliz”, ou seja, apenas as partes mais importantes;
- Regreção: São testes que garantem que os demais códigos ainda funcionam após as alterações. Ou seja, servem para garantir compatibilidade dentro do código;
- Aceitação: Até aqui, os testes garantem que o código funciona. Esse é o teste que garante que o código faz o que deveria fazer, ou seja, atende os requisitos do projeto;
- Automatizados: São testes que podem ser executados de forma automatizada, sem necessidade de interação. Muito utilizados em processos de integração contínua (IC).
Não sou profissional se não testo?
Os testes existem por um objetivo principal: atestar qualidade. Quando escrevemos testes, queremos garantir que nosso código funciona. Podemos verificar se nosso código é robusto e está preparado para quando a situação não é ideal (parâmetros errados, falha de conexão, etc.). Os testes, hoje, talvez sejam a maneira mais facil e comum de se atestar qualidade. A não ser que você tenha outra maneira de fazer isso, escrever testes é um bom sinal de profissionalismo (mas não o garante).
TDD?
Desenvolvimento guiado por testes (Test-driven development, ou TDD), é um processo de se escrever testes antes do próprio codigo ser escrito, o que, supostamente, força o desenvolvedor a refletir sobre o código e por resultado, escreve um código de melhor qualidade. O real benefício do TDD é bastante discutivel, principalmente pelo fato de qualidade ser um atributo bastante subjetivo. Porém, é uma discussão que não cabe nesse post.
Um ponto importante é que, principalmente conforme nossa experiencia aumenta, não necessáriamente precisamos escrever esse teste antes para obter os beneficios. Muitas vezes, antes de começar, já temos uma ideia do que código que queremos escrever e conseguimos guiá-lo a uma melhor qualidade. O que acaba por ser um processo de escrever os testes simultaneamente, nem antes, nem depois.