DEV Community

Wesley Matos
Wesley Matos

Posted on • Updated on

O básico sobre testes unitários e dublês de testes

O que são testes unitários?

Teste unitário, como o nome sugere, é o teste de uma unidade do sistema, ou seja, de um pedaço do seu código. Não é como um teste de ponta-a-ponta, onde todo o sistema é testado com alguma automatização ou até mesmo de maneira manual (com uma pessoa preenchendo os campos, clicando nos botões, etc).

Um dos grandes objetivos das equipes é reaproveitar o máximo de código possível... Como garantir que um código que será utilizado em vários lugares do seu sistema está funcionando corretamente? Com testes unitários! Os testes unitários irão garantir a confiabilidade do seu sistema mesmo após mudanças (em caso de problema, os testes irão quebrar, e assim você saberá que sua implementação não está funcionando como deveria).

Para fim de exemplos, vamos criar uma função que chama outra função. Vamos chamá-la de proxy

export const proxy = (method, ...args) => method(...args)
Enter fullscreen mode Exit fullscreen mode

Um caminho para testar a função proxy sem perder tempo criando um método para ser passado pra ela ou pensando em parâmetros para essa função, seria usar dublês de testes.

O que são dublês de testes

Dentro dos conceitos essenciais para dominar os testes unitários, estão os dublês de testes. Hoje vamos falar um pouco sobre o que são os dublês de testes.

Assim como dublês de filmes estão para atores principais, os dublês de testes estão para as implementações reais.

Tipos de dublês

Basicamente, existem 5 tipos de dublês de testes. Sendo eles, dummies, fakes, stubs, spies e mocks.

Dummies

A tradução literal seria "bobos", ou seja, são argumentos que não tem um valor real. Aqui vai um exemplo para tornar tudo mais claro:

// temos a função "sum", uma função que a soma de dois números
const sum = (a, b) => a + b;

// na minha visão, existem duas formas básicas
// que podemos usar pra implementar dummies

// criar uma lista de parâmetros para serem passados
// pra uma função específica
const sumDummies = [1, 2]

// OU

// criar os dummies baseados nos tipos
// se eu tenho um tipo number, então terei um dummy pra números
// e em todos os lugares onde for necessário passar números
// como parametros, utilizaremos o mesmo dummy
const numberDummy = 2
Enter fullscreen mode Exit fullscreen mode
Fakes

Fakes são objetos com implementações funcionais que geralmente são utilizados como dependências de alguma unidade sendo testada. Vamos para o exemplo mais comum de fake, uma implementação de banco de dados em memória

// imagine que temos uma service que precisa acessar uma implementação
// de banco de dados que tenha os métodos count, save e find
// criaremos uma implementação falsa, que implemente esses
// métodos, para poder simular o funcionamento do sistema
// sem ter o banco de dados de pé

class FakeDatabase {
  #data = []

  save(params) {
    this.#data.push(params)
  }

  find(query) {
    return this.#data.find(item => item === query)
  }

  count(query) {
    return this.#data.filter(item => item === query).length
  }
}

// essa implementação, mesmo com sua simplicidade, consegue simular
// o comportamento de uma service que faz algo no banco de dados.
// utilizando essa implementação seria possível fazer os testes
// necessários sem precisar subir a instância do banco e podendo
// ainda assim dar um "count" nos dados salvos, ou até mesmo buscar
// uma informação salva previamente
Enter fullscreen mode Exit fullscreen mode
Stubs

Assim como o fake, a função do stub também é substituir implementações reais pro testes ser rodado. A diferença é que ele não tem complexidade alguma, é só uma função falsa que retorna um valor específico, independente dos parâmetros passados pra ela

// para esse exemplo, usaremos o método proxy novamente...
// imagine que queremos testar o método proxy, e ter certeza que o
// que ele retorna é a mesma coisa que o método passado pra ele retorna
// o que podemos fazer, é criar um método falso que tem um valor
// de retorno padrão
const sumStub = () => 5

// o método "sumStub", poderia substituir o método "sum" numa injeção
// de dependência, mas ele não se preocupa com o valor recebido, ele
// apenas retorna o valor que decidirmos ser relevante praquele caso de
// teste específico
Enter fullscreen mode Exit fullscreen mode
Spies

Podemos dizer que o spy é um "stub com memória". Assim como o stub, ele tem o retorno de um valor pré-programado, mas ele também guarda as informações de como ele foi chamado, quantas vezes foi chamado, etc

Geralmente são utilizadas bibliotecas pra criar spies, então vou deixar o link de exemplos de implementação das libs que utilizo:

Mocks

São objetos que além de terem um retorno pré-programado, tem também suas chamadas pré-programadas. Então ao contrário do stub, que não se importa em como você o chama ou quais parâmetros passa, o mock também checa se você está passando os valores certos pra receber aquele retorno esperado.

Mocks e stubs são iguais?

Já dizia Martin Fowler: Mocks não são stubs, porque enquanto stubs têm apenas retornos pré-programados, que serão retornados independente dos parâmetros recebidos, mocks são objetos com uma expectativa em relação aos argumentos que receberá.

Infelizmente o Jest (biblioteca de testes criada pelo Facebook e uma das mais utilizadas em ambientes JavaScript), não implementa mocks, então nesse exemplo vamos usar o sinon.js para exemplificar a diferença entre um e outro.

Para este exemplo, usaremos o método proxy novamente

export const proxy = (method, ...args) => method(...args)
Enter fullscreen mode Exit fullscreen mode

Vamos testar o método proxy agora

  • Primeiro, injetando o um stub como método

    const obj = { add }
    
    const addStub = sinon.stub(obj, 'add')
    
    const data = proxy(obj.add)(2, 3)
    
    sinon.assert.calledOnceWithExactly(addStub, 2, 3)
    sinon.assert.match(data, 5)
    
  • Agora, o mesmo caso de uso implementado utilizando mock

    const obj = { add }
    
    const mock = sinon.mock(obj)
    
    const addMock = mock.expects('add').withExactArgs(2, 3)
    
    const data = proxy(obj.add)(2, 3)
    
    addMock.verify()
    
    sinon.assert.match(data, 5)
    

A diferença entre o mock e o stub, é o fluxo de teste deles. Enquanto o fluxo do stub é setup -> execute -> verify, o fluxo do mock é setup (object) -> setup (expectations) -> execute -> verify (mock) -> verify (spies, stubs, anything that isn't a mock).

Como organizar meus testes?

Provavelmente existem muitas formas de fazer isso, mas a que mais me agrada e a que utilizo no dia-a-dia, é a metodologia "Triple A", ou "AAA". Os três As significam:

  • Arrange
  • Act
  • Assert

Primeiro, você faz o "Arrange", ou seja, você prepara tudo pra que seu teste seja bem sucedido. Você utiliza esse espaço para criar variáveis, crias instâncias, spies, etc.

O segundo passo, é o "Act", ou seja, agir/executar. É onde você executa as funções e utiliza os gatilhos para testar aquele caso específico.

Terceiro passo, "Assert", ou seja, é o momento onde você vai validar que o comportamento foi de fato o que você esperava. É onde você valida se as funções corretas forma chamados com os parâmetros corretos, se o retorno do método testado foi o que você esperava, verifica os mocks, etc.

O que mais me atrai no "Triple A", é que é uma forma simples, organizada e intuitiva de escrever os testes. Se tranquilize se não conseguir fazer dessa forma logo no início, o que importa é tentar se habituar. Uma coisa que fiz muito para iniciar, foi criar comentários. Sempre que escrevia um caso de teste, primeiro fazia isso:

  it('example case', () => {
    // arrange

    // act

    // assert
  })
Enter fullscreen mode Exit fullscreen mode

Com esse pequeno trecho de código escrito, só você fazer as coisas na ordem. Primeiro você arranja tudo que precisa, depois executa, e depois verifica se tudo deu certo.

Espero que essa metodologia possa te ajudar a escrever testes mais consistentes e claros!

Caso tenha restado alguma dúvida, aqui vai o código que fizemos como exemplo para testar o stub com os comentários do AAA:

  it('example case', () => {
    // arrange
    const obj = { add }
    const mock = sinon.mock(obj)
    const addMock = mock.expects('add').withExactArgs(2, 3)

    // act
    const data = proxy(obj.add)(2, 3)

    // assert
    addMock.verify()
    sinon.assert.match(data, 5)
  })
Enter fullscreen mode Exit fullscreen mode

O que devemos testar?

Uma das coisas que mais tive dificuldade quando iniciei com os testes unitários, foi saber o que é importante ser testado e o que não é. Ouvimos muito falar do famoso (e mítico) "100% de coverage", mas ter 100% de coverage não garante que sua aplicação vai funcionar corretamente em produção.

Testes unitários são testes que garantem a confiabilidade das unidades do sistema, então o ideal, é que você aproveite os testes unitários para testar regras de negócio, coisas relevantes para o negócio.

Imagine que a pessoa decide o que vai ser feito (seu chefe, o PO, seu tech lead, etc), te diz que você precisa criar uma rota de cadastro na API que já existe no sistema que você trabalha. O que você faz? Descobre como o sistema tem que se comportar na determinada situação. Aqui vão alguns exemplos:

  • Existe uma idade mínima pros usuários serem cadastrados?
  • Quais informações são necessárias para o cadastro do usuário?
  • Em caso de informações vazias, como o sistema deve se comportar?

Geralmente, esses itens que falam sobre como o sistema tem que se comportar em qual situação, são chamados de "Regras de Negócio". As regras de negócio, são as regras que determinam como o sistema deve se comportar e agir em determinadas situações, testá-las é o que dá confiabilidade pro sistema que você desenvolve.

Existe um ganho imenso quando o seu caso de teste é "should make xxx when xxx because of xxx" (imagine que esses "xxx" seriam as regras específicas de negócio que você está testando, por exemplo, que o usuário deve ter mais de 18 anos) ao invés de "should call method xxx".

Uma curiosidade é que o ".spec" como sufixo de arquivos de testes em algumas libs de teste do javascript, vem da palavra "specification", ou seja, "especificação". O que isso pode nos ensinar? Que os testes unitários também podem servir como um tipo de especificação, ou documentação pra unidade do seu sistema!

Quando estamos desenvolvendo, não é natural pensar "esse teste tem que explicar esse código", mas quando precisamos dar manutenção em um código, e os casos de teste descrevem não só o que o código faz, mas também o porquê ele faz.

Testar as regras de negócio é um ótimo caminho pra escrever testes cada vez mais confiáveis. Quando você tem as regras de negócio testadas, se for necessária alguma manutenção ou refatoração no sistema, talvez exista alguma complexidade de código que exista ali e você já pense logo "meu deus, um for, vou remover isso agora e esse método vai ficar muito mais rápido!", mas o que não pensamos, é que 6 meses atrás, quando aquela função foi implementada, existia um requisito de que o sistema precisava precisava tratar as informações uma a uma, sequencialmente, porque pode existir algum sistema de outro time que precisa que as coisas funcionem dessa forma.

Criar testes que validem o que é mais importante pro projeto (esse usuário tem todas as informações e dependencias necessarias para finalizar o cadastro? ele já é cadastrado no sistema? o e-mail foi enviado pra ele corretamente depois de salvá-lo no banco de dados?), e não apenas o que é importante pra implementação (chamar função x, receber parâmetro y), é uma das melhores formas de entregar valor pro seu projeto e pro seu time!

Agradecimentos

Desenvolver isso foi de um ganho imenso pra mim, e espero que ler isso te traga esse ganho também!

Obrigado por disponibilizar seu tempo pra prestigiar meu trabalho :)

Se tiver alguma dúvida, sugestão, quiser me conhecer melhor ou criar uma conexão, sinta-se a vontade para me uma mensagem no LinkedIn.

Referências

Top comments (0)