O que você precisa saber sobre Testes Unitários

Principalmente em um ambiente ágil.

Após uma reunião de desenvolvedores, surgiu o que conhecemos como manifesto ágil, essa reunião aconteceu após eles discutirem os métodos que faziam ter sucesso na entrega de software enquanto havia falhas em outros projetos.

Um desses métodos é o Extreme Programming, criado por Kent Beck um dos signatários do manifesto ágil. Uma parte importante do Extreme Programming é orientar o desenvolvimento por testes ou TDD (abreviação do inglês).

TDD

Para se manter ágil em desenvolvimento de software é necessário que sejam feitas constantes mudanças no código e para que essa possibilidade exista, precisamos lutar para que o software não apodreça, por isso a importancia de guiar o desenvolvimento por testes.

No TDD começamos com a escrita de testes unitários, seguido pela escrita do código que faça do teste passar, com isso você vai adicionando mais testes e mais código, criando códigos concisos para o seus testes passarem.

Como parte ainda do TDD eles precisam ser automatizado, nem todo teste unitário é automatizado, mas para o TDD precisa já que será feito durante a escrita de código.

Testes Unitários

O que são testes unitários ou unidades? A definição de uma unidade geralmente se refere a classe e seus métodos. Isso é importante notar, porque traz outro pronto importante sobre o testes. Uma unidade dificil de testar é um sinal de que precisa de uma refatoração, um ponto importante do TDD.

A refatoração ajuda a manter o código simples e conciso é a forma de lutar contra a podridão do software (ou code smells). Sempre que algo ficou difícil de testar existe um claro sinal de que precisa ser repensado, fazer isso leva a uma arquitetura melhor. E uma arquitetura melhor faz parte da atenção continua à excelência técnica que aumenta agilidade.

Isolamento

O teste unitário, não se preocupa com coisas além da unidade testada, ele precisa ser isolado de conexões com o banco, APIs externas e até outros módulos. Essas conexões devem ser cobertas usando testes de integração para garantir o bom funcionamento das partes.

Como parte do isolamento, alguns códigos podem ser difícil de testar e código difícil de testar sinaliza problemas na arquitetura. Para resolver isso de forma elegante e tornar um código fácil de testar, um caminho é escolher uma arquitetura em que tenhamos injeção de dependências.

Spies, Stubs e Mocks

Para facilitar nos testes de unidades dependentes de outras unidades, usar Spies, Stubs e Mocks ajuda na criação e também validações feitas pelo teste, existe também outros para serem usados. Vamos ver a característica de cada um.

  • Spy

O Spy pode ser usado para validar as chamadas externas expondo os parâmetros dessa chamada, registrando cada chamada para serem validados nos testes.

  • Stub

Você pode adicionar uma certa lógica e retornar um valor é muito útil para testar códigos assíncronos e conexões com banco de dados.

  • Mocks

Ele é definido para conter alguma lógica e também modificações de acordo com a sequencia de execução das chamadas.

BDD

Apesar de já termos abordado algumas boas práticas do TDD, não é só ele que envolve testes unidades. O BDD também foi criado para facilitar a escrita de testes unitários por iniciantes a prática do TDD e é focado mais no comportamento de uma unidade do que na execução detalhada de suas operações.

O BDD é escrito de forma mais declarativa assim as modificações no software afetam tanto os testes unitários.

Um exemplo de teste em TDD:

suite('Contador', () => {
  test('tick aumenta contador em 1', () => {
    let counter = new Counter();
    counter.tick();
    assert.equal(counter.count, 1);
  });
});

Agora com BDD

describe('Contador', () => {
  it('aumenta o contador em 1 após o tick', () => {
    const counter = new Counter();
    const count = counter.count;
    const expectedCount = count + 1;
    counter.tick();
    expect(counter.count).to.be.equal(expectedCount);
  });
});

Observe a leitura do valor e uma modificação interna no teste, antes de validar o que é esperado.

Boas Práticas

Apesar de já termos abordado algumas das boas práticas, ainda temos algumas outras para falar. Usar a sequencia de construção dos testes unitários é muito importante, a sequencio é o Organize, Atue e Cheque.

Essa sequencia tem a intenção de evitar mudanças de comportamento ou até mesmo dos testes durante sua execução. Não levar em conta pode produzir desperdicio do tipo testar o dado e não o código produzido.

Pense no fluxo de Organize Atue e Cheque tanto ao escrever um cenário quando ao escrever todo o teste, preparar a unidade testada é importante e garante um comportamento determinístico ao que foi escrito, evitar efeitos colaterais ajuda muito e também economiza tempo evitando testes desnecessários. Por exemplo: ‌‌‌

describe('Contador', () => {
   it('aumenta o contador em 1 após o tick', () => {
       const counter = new Counter();
       let expectedCount = 1;
       counter.tick();
       expect(counter.count).to.be.equal(expectedCount);
       expectedCount = 2;
       counter.tick();
       expect(counter.count).to.be.equal(expectedCount);
  });
 });

O teste acima por exemplo, olha para algo desnecessário.

Dê preferência ao uso da terminologia do BDD de describe e it, isso facilita que os testes já sejam escritos de forma mais declarativa e possa servir de documentação viva para o projeto.

Refatore constantemente, refatorar é uma parte importante para se manter a qualidade, evitar que seu código apodreça e o projeto precise ser descartado com o tempo por tornar a manutenção insustentável. Refatorar é economia.

Más práticas

Assim como algumas boas prática abordadas antes também falamos de algumas outras más práticas, mas precisamos expandir alguns pontos e que podem servir de atenção caso esteja acontecendo, isso sinaliza problemas nos testes ou software escrito.

Falta de clareza no testes escritos, as vezes um teste está com muitas responsabilidades e não declarativo sobre o que ele realmente está testando, isso pode ser sinal de um problema ainda maior em que a função testada está com muitas responsabilidade. Uma forma disso acontecer é que acaba gerando

Teste integrado ao invés de teste unitário, esse é um erro comum e até frequente, principalmente quando começamos a fazer testes unitários é esquecer de separar coisas como chamadas do banco de dados o que faz os testes serem testes integrados.

Dependência entre cenários de testes. Seu teste não pode depender de uma execução prévia para passar, esse tipo de teste se torna complexo ainda mais em cenários de refatoração.

Não ter revisão do código feita nos testes unitários, isso faz com que os testes não sejam criados se preocupando com manutenções futuras e falha em trazer uma documentação viva ao projeto.

Criar lógica dentro dos testes, essa eu diria que é uma péssima prática, criar lógica dentro dos testes, deixa o teste com leitura complicada e possivelmente está mascarando um problema.

Finalizando

Testes unitários é só usado com TDD? Não, você pode escrever testes unitários após desenvolver uma funcionalidade ou quando se precisa refatorar algo sem testes, mas idealmente é que os testes cresçam com o projeto.

Só faz testes unitários se estiver usando XP? Apesar de ter nascido com ele, testes unitários é importante sempre que você escrever software e não é preso ao uso de XP.

Se formos tratar de front-end, ainda precisamos considerar uma nova prática de teste unitário, que é o teste unitário de superficie, no qual testa as funcionalidade de uma view, isso tem sido uma caracteristica introduziada pelo react e seguida por outros frameworks. Um pouco mais sobre isso pode ser encontrado neste artigo.

Testes unitários é um assunto complexo e extenso, teria ainda muito mais para falar ou se aprofundar, mas espero ter explicado um pouco do surgimento dos testes unitários e sua importancia.

Algumas Referencias