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.
Os campos possuem algumas validações e caso sejam descumpridas, mensagens de erro são exibidas.
Com todas as validações ok chamamos uma função assíncrona que aleatoriamente vai retornar erro
Ou sucesso, com o nome do usuário criado
Durante a execução da função um estado de loading é exibido
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:
Que ao rodar falha, uma vez que ainda não implementamos o validador.
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
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.
Após a execução vamos construir esta parte do validador
Rodar os testes e conferir o resultado:
Ó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.
Ver sua execução falhar.
Implementar a funcionalidade que resolva o problema,
E rodar os testes novamente a fim de verificar se passam
Finalmente podemos trabalhar no último requisito, retornar isValid true e messages empty caso todas as validações sejam atendidas. Vamos novamente repetir o processo.
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.
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
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
Com as validações de erro sendo exibidas para o usuário, podemos partir para a senha, novamente vamos adicionar um teste.
Esperar sua falha.
Refatorar a função handleFormSubmit no componente Login para validar a senha
Precisamos limpar as mensagens caso o usuário corrija todos os erros e envie o formulário novamente. Para isso vamos adicionar o teste.
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.
Para fazer este teste passar vamos refatorar a função handleFormSubmit novamente a fim de chamar a criação de usuário.
Com este requisito atendido vamos construir o loading, para isso vamos mockar o retorno do useCreateUser em loading e construir o teste
Para fazer este teste passar vamos ter que refatorar nosso componente, primeiro vamos extrair a propriedade isLoading exposta pelo hook
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.