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.