Entenda TDD com exemplo prático

09 set 2024

No último artigo, Criando Testes Unitários e de Integração no Frontend, discutimos como adicionar testes a uma aplicação. Agora, vamos explorar uma metodologia onde as aplicações são construídas a partir dos testes.

O Desenvolvimento Orientado por Testes (TDD - Test Driven Development) é uma metodologia de desenvolvimento de software em que os testes são escritos antes da implementação do código funcional. O processo começa com a criação de um teste que descreve uma nova funcionalidade ou melhoria. Em seguida, escreve-se o código mínimo necessário para que o teste passe. Esse ciclo de escrever testes, implementar código e refatorar é repetido até que todos os requisitos sejam atendidos. Essa abordagem contínua promove um software mais limpo e robusto, além de garantir que as funcionalidades sejam testadas de maneira automatizada e constante.

A grande sacada do TDD é incentivar os desenvolvedores a pensarem de forma mais profunda e planejarem melhor o código. Como o teste é escrito primeiro, asseguramos que o código abrange tanto cenários de sucesso quanto de falha, atendendo mais eficientemente os critérios de aceitação das histórias. Com um código mais bem planejado e uma ampla cobertura de testes, alcançamos maior robustez, menos bugs e uma manutenção mais fácil.

A mudança de mentalidade entre escrever os testes antes ou depois do código pode envolver uma curva significativa de aprendizado, especialmente para desenvolvedores que não estão acostumados a escrever testes. No início do projeto, pode parecer que o desenvolvimento é mais lento devido ao volume de testes necessários. No entanto, é importante lembrar que o número de bugs e o tempo gasto em manutenção são drasticamente reduzidos.

Além disso, o código dos testes não deve ser tratado como de segunda classe; a qualidade e clareza dos testes são tão importantes quanto a do código da funcionalidade. Afinal, a cada nova interação sobre uma funcionalidade existente, podemos quebrar algum teste e ter que refatorá-lo.

 

Exemplo Prático

Para clarificar os conceitos acima temos um app de exemplo, que simula uma página de cadastro de usuários.

1 (1)

Os campos possuem algumas validações e caso sejam descumpridas, mensagens de erro são exibidas.

2 (1)

Com todas as validações ok chamamos uma função assíncrona que aleatoriamente vai retornar erro

 

3 (1)

Ou sucesso, com o nome do usuário criado


4

Durante a execução da função um estado de loading é exibido

5-1

Vamos construir este app juntos para exemplificar o uso da metodologia TDD.

Uma boa pergunta seria, por onde começar esse app? Podemos começar pela lógica dos validadores, são partes que podem ser facilmente desacopladas e testadas. Começamos pelo teste mais simples, escrevendo a mínima funcionalidade que o faça passar e repetir este processo até que os requisitos sejam atendidos.

Nossas validações para o usuário são:

- Ter entre 4 e 12 caracteres

- Começar com letra

- Caso todas as condições sejam atendidas retornar sucesso

 

Para isso o validador retornará um objeto com 2 campos

 

  • isValid: booleano indicando resultado da validação 
  • messages: array de strings contendo mensagens de erro para cada validação não atendida

 

Deste modo escrevemos o primeiro teste:

Captura de tela 2024-09-09 104405

Que ao rodar falha, uma vez que ainda não implementamos o validador.

6 (2)

Captura de tela 2024-09-09 105558

Sempre rode os testes antes de implementar o código, isto é uma boa maneira de ter certeza que eles são capazes de quebrar. Um teste que nunca falha não testa nada.

Vamos agora ao nosso validador de usuário e implementar o código necessário para o teste passar

Captura de tela 2024-09-09 105701

Vamos repetir esse processo até que o validador de usuário esteja completo. Agora, vamos adicionar um teste para verificar o tamanho máximo permitido para o usuário.

Captura de tela 2024-09-09 105806

7 (2)

Após a execução vamos construir esta parte do validador

Captura de tela 2024-09-09 105926


Rodar os testes e conferir o resultado:

image

Ótimo, agora temos certeza de que nosso validador verifica se o nome de usuário tem entre 4 e 12 caracteres. Podemos, então, avançar para verificar se o nome começa com uma letra. Para isso, vamos adicionar mais um teste.


Captura de tela 2024-09-09 110100

Ver sua execução falhar.

image (1)

Implementar a funcionalidade que resolva o problema,

Captura de tela 2024-09-09 110232

E rodar os testes novamente a fim de verificar se passam

10 (1)

Finalmente podemos trabalhar no último requisito, retornar isValid true e messages empty caso todas as validações sejam atendidas. Vamos novamente repetir o processo.

Captura de tela 2024-09-09 110618

11 (1)

Devido ao design do validador este teste passou de primeira, pois isValid começa como true e messages vazio, se nenhuma condição for cumprida as variáveis continuam da forma que começaram. Para ter certeza que este teste é útil vou passar um usuário inválido e verificar se ele quebra.

Captura de tela 2024-09-09 110825

12 (1)

Passei 1rafael que fere uma das condições e o teste de sucesso quebrou! isValid se tornou false. Pronto! Garantimos o bom funcionamento de todos os testes.

Para construir o validador de senhas seguiremos exatamente o mesmo processo, ao final teremos como resultado o arquivo de testes e o validador

Captura de tela 2024-09-09 111001Captura de tela 2024-09-09 111049

Com os validadores construídos e testados é hora de começar a tela de cadastro, seus requisitos são:

  • Mostrar mensagens de erro dos validadores.
  • Limpar mensagens após usuário submeter dados válidos.
  • Mostrar a string 'Loading …' no lugar do botão de enviar enquanto a criação de usuário ocorre.
  • Mostrar mensagens de erro e sucesso de acordo com o resultado do cadastro de usuário.

para isso vamos criar o arquivo Login.test.tsx e adicionar nosso primeiro teste:

Para satisfazer o teste temos a primeira iteração na tela de Login

Captura de tela 2024-09-09 111334

Captura de tela 2024-09-09 111442

Com as validações de erro sendo exibidas para o usuário, podemos partir para a senha, novamente vamos adicionar um teste.

Captura de tela 2024-09-09 111545

Captura de tela 2024-09-09 111648

Esperar sua falha.

13

Refatorar a função handleFormSubmit no componente Login para validar a senha

Captura de tela 2024-09-09 111858

Precisamos limpar as mensagens caso o usuário corrija todos os erros e envie o formulário novamente. Para isso vamos adicionar o teste.

Captura de tela 2024-09-09 112004

Para fazê lo passar precisamos novamente refatorar a função handleFormSubmit 

Agora, caso todas as validações sejam satisfeitas deve se chamar a função assíncrona de criar usuário. Não vou entrar nos detalhes da implementação desta função pois o objetivo dela é simplesmente simular uma chamada à uma API. Esta função recebe usuário e senha e aleatoriamente retorna sucesso com os dados do usuário criado ou erro.

Para começar vamos criar um teste, onde após o clique no botão enviar, caso todas as validações estejam satisfeitas esperamos que a função createUser exposta pelo hook useCreateUser seja chamada.

No teste estamos criando uma mock function e passando ela como mock do valor de retorno do hook, deste modo podemos verificar se nossa mock function foi chamada e por consequência a função createUser do hook useCreateUser.

Captura de tela 2024-09-09 112211

Para fazer este teste passar vamos refatorar a função handleFormSubmit novamente a fim de chamar a criação de usuário.

Captura de tela 2024-09-09 112322

Com este requisito atendido vamos construir o loading, para isso vamos mockar o retorno do useCreateUser em loading e construir o teste

Captura de tela 2024-09-09 113216

Para fazer este teste passar vamos ter que refatorar nosso componente, primeiro vamos extrair a propriedade isLoading exposta pelo hook


Captura de tela 2024-09-09 113258


e depois  criar um ternário no template para condicionalmente mostrar nosso loading no lugar do botão

Para o requisito das mensagens de erro ao criar o usuário vamos usar o mesmo raciocínio, o useCreateHook expõe uma propriedade failed, caso a função não esteja aguardando (isLoading é false) e failed seja  true significa que houve um erro na chamada assíncrona e uma mensagem deve ser exibida ao usuário.

Para verificar isso vamos criar o teste:

Para fazer com que passe, vamos extrair a propriedade failed de useCreateUser.

e adicionar no template o banner de erro

Finalmente podemos atender ao requisito de mostrar a uma mensagem de sucesso com o usuário criado, este teste será similar ao anterior, vamos mockar a chamada assíncrona no estado de sucesso e procurar na tela a mensagem esperada.

Para fazer este teste passar, vamos extrair a propriedade response do hook de criação de usuário e adicionar o valor da propriedade username no banner.

Pronto! Nosso app foi criado e testado com sucesso. Ao rodar o projeto, verificamos que todos os comportamentos estão funcionando corretamente. Vale destacar que, para garantir esse funcionamento, não foi necessário executar o app manualmente. Com testes automatizados bem planejados, asseguramos que o sistema inteiro esteja operando corretamente antes mesmo de ser executado. E o melhor: em futuras evoluções, se algum comportamento for comprometido, os testes irão detectar o problema imediatamente, facilitando refatorações e prevenindo bugs.

Sim, TDD exige uma curva de aprendizado, mas é uma ferramenta poderosíssima para construção de código robusto e de qualidade.