Há algum tempo existe resistência ao desenvolvimento baseado em Test Driven Development (TDD). Geralmente, por necessidade de entrega rápida de uma funcionalidade para o negócio, há uma pressão em deixar os testes para um segundo momento.
Fazer os testes após o desenvolvimento é uma abordagem válida, que trás como vantagem a rápida entrega da funcionalidade, mas por outro lado se torna mais complexa de ser feita, por ter que revisitar cenários da regra de negócio e fica mais difícil convencer os donos do projeto que é uma ação necessária, pois se está funcionando, por que será “gasto” um tempo com testes?
Aplicações sem testes unitários podem ser difíceis de manter, alterações podem causar efeitos colaterais e conforme o tempo de vida da aplicação vai aumentando, os problemas aumentam.
A mentalidade TDD
A pergunta que vem à cabeça quando falamos de TDD é “como vou testar algo que nem foi feito ainda?”. No primeiro momento, o TDD vai ajudar o desenvolvedor a validar as regras de negócio que a aplicação vai atender, além de ajudar a direcionar o desenvolvimento da aplicação.
Regra de negócio
Antes de começar a desenvolver uma aplicação, seja através do TDD, seja através da abordagem tradicional, é necessário entender qual problema a aplicação vai resolver. Quanto mais refinada estiver a regra de negócio, maiores são as chances de sucesso da aplicação. Para este exemplo, a regra de negócio será baseada em uma tabela de preço de estacionamento. Onde:
- Cada período corresponde a 1 hora.
- 1 diária corresponde a 24 horas.
- De segunda a sexta, o primeiro período tem valor fixo, os demais períodos têm valor somados até completar 1 diária. Após a primeira diária, o excedente vai ser cobrado em diárias. Se o veículo ficou estacionado por 25 horas, serão cobradas duas diárias
- Sábado, o valor é fixo.
- Domingo o estacionamento não funciona.
Cenários
Sabendo a regra de negócio, o próximo passo é criar cenários de testes que cubram todas as regras possíveis, em caso de sucesso e falha. Sendo assim, uma proposta de cenários seria:
- De segunda a sexta, o valor do primeiro período é de $5, os períodos até completar uma diária têm valor de $2. Cada diária tem valor de $25.
- Aos sábados, o valor é de $15, independente do tempo de estacionamento.
Entrada | Saída | Tempo | Valor | Cálculo |
Seg-09:00 | Seg-10:29 | 1:29 | $7 | 1 hora=$5 + 0:29=$2 |
Seg-12:10 | Seg-12:25 | 0:15 | $5 | 15 minutos=$5 |
Ter-10:25 | Qua-12:10 | 25:45 | $50 | 1 diária=$25 + 1:45=$25 |
Sáb-09:00 | Sab-12:04 | 03:04 | $15 | Valor fixo |
Dom-10:15 | Dom-12:15 | 02:00 | Não calcula |
Escrevendo os primeiros testes
Com os cenários definidos, é o momento de começar a escrever os testes. O ideal é que sejam cobertos a maioria dos cenários, englobando tanto sucessos quanto falhas. Ao aplicar o TDD podemos seguir o ciclo RED, GREEN, REFACTOR. Onde:
- RED: o teste é implementado, e não deve passar por que não tem uma implementação.
- GREEN: apenas o código básico para fazer o teste passar é implementado.
- REFACTOR: o código é analisado e melhorado.
Ainda aproveitando-se dos cenários definidos já é possível tomar algumas decisões relacionadas ao código. Algumas perguntas ajudam o desenvolvimento dos testes, tornando-os mais consistentes e diminuindo a chance de grandes mudanças. As perguntas que podem ser feitas são:
- Quais serão os parâmetros de entrada do teste?
- Quais serão os valores esperados após executar a ação?
- Há valores que serão obtidos ou enviados para bases de dados ou serviços externos?
Respondendo às perguntas:
- Para atender a esta regra de negócio, a aplicação deverá receber a data e hora de entrada e saída do veículo.
- Para calcular o valor a ser pago, a aplicação fará uma subtração da hora de entrada e de saída, carregará a tabela com os valores cadastrada previamente e irá calcular o valor a ser pago.
- Os valores esperados após o cálculo é o valor cadastrado na tabela de preço de acordo com o período de utilização.
Testes em RED
Com estas informações já é possível escrever os testes em RED. Eles não executarão por que não há código implementado, mas já estarão estruturados de forma a direcionar o desenvolvimento do código:
Analisando brevemente o código, há um teste que calcula com sucesso e o outro que calcula com falha. Ambos criam um objeto Ticket que irá receber os parâmetros de entrada e saída. O objeto Ticket pode calcular a duração da estadia no estacionamento. Em seguida, um serviço PricingService é responsável por calcular o valor do ticket. Este serviço, quando implementado, vai consultar um repositório para carregar os parâmetros de cálculo da tabela de preço. E por último, é feita uma validação para garantir que o valor calculado é o esperado no caso do teste de sucesso, ou se retornou alguma mensagem de erro.
Anatomia do Teste Unitário
Os testes devem ser criados levando em consideração que dadas tais condições (arrange), ao executar tal ação (act), tais resultados devem ser retornados (assert). Isso é chamado de AAA onde:
- Arrange: são definidos os parâmetros de entrada do teste.
- Act: ação executada com os parâmetros.
- Assert: validação dos resultados gerados pela ação.
Esta forma de desenvolver o teste unitário é bem intuitiva, deixando claro cada passo do teste.
Mudando os testes para GREEN
Os testes estão estruturados mas não funcionam, então é o momento de gerar o mínimo de código para o teste rodar. Desta forma serão criados os objetos necessários para as primeiras linhas de codificação do domínio:
Este trecho de código cobre os cenários propostos, e fazem os testes executarem com sucesso.
REFACTOR
Neste momento, os testes já estão criados e validam as regras com código mockado. A partir deste momento, o código pode ser refatorado para aplicar lógica para atender a regra de negócio.
O ganho neste momento é que durante o processo de refatoração os testes podem ser executados para identificar se está tudo funcionando como planejado.
Como estes testes estão validando a regra de negócio, e há a necessidade de retornar dados de uma fonte externa, é possível mockar o retorno da base dados, para que os testes sejam executados.
Como o repositório é passado na construção do service através de sua interface, uma classe pode ser escrita para retornar os dados esperados pelo teste.
Há diversas bibliotecas de mock para facilitar este processo, geralmente utilizados em testes mais complexos. Para este cenário simples, escrever uma classe que retorne o que é necessário para validar a lógica do serviço é o suficiente.
Com o repositório criado, o método de cálculo pode ser reescrito para a lógica final, descartando o código anterior feito para retornar os valores esperados.
Este código é a lógica final implementada, ao executar os testes unitários, eles retornam sucesso.
Se for necessário implementar uma nova funcionalidade, o ideal é fazer novamente o teste unitário em RED, passar pelo GREEN e depois REFACTOR, isso se torna um ciclo contínuo.
Se for necessário uma correção nesta lógica, basta fazer a alteração e executar o teste novamente.
Aplicando novas regras
Com o tempo é natural que ocorram alterações nas regras da aplicação. Ao trabalhar com TDD, o primeiro pensamento deve ser verificar se o teste unitário precisa de alteração, ou que sejam criados novos, e depois partir para a alteração. Como exemplo, vamos simular algumas alterações:
Mudança na regra #1
Por uma decisão da diretoria, o estacionamento passará a funcionar com tabela especial toda segunda-feira. Esta tabela terá um valor incremental, a cada hora será cobrado $3. Com os testes já criados, basta alterar o teste de segunda-feira e adicionar um novo retorno ao mock do banco de dados. A imagem abaixo mostra a alteração nos dois primeiros cenários de segunda-feira.
A imagem seguinte mostra que o repositório retorna uma tabela especial para segunda-feira.
A lógica não teve alteração, os dados de entrada e esperados foram alterados e os testes continuam passando.
Mudança de Regra #2
A direção do estacionamento decidiu que os carros podem sair dentro de um período de tolerância sem pagar. Seguindo o mesmo formato do teste anterior, basta criar um cenário em que a diferença entre a entrada e saída esteja na tolerância. Neste momento o teste ficará em RED porque a regra não foi implementada, conforme mostra a imagem abaixo:
Implementando a lógica, é possível já fazer os passos GREEN e REFACTOR.
E agora os testes passam a funcionar.
Quer alavancar sua carreira como Desenvolvedor(a) .NET?
Além de Desenvolvedor .NET Sênior, eu sou instrutor de mais de 700 alunos e também tenho dezenas de mentorados.
Conheça o Método .NET Direto ao Ponto, minha plataforma com mais de 800 videoaulas, com cursos cobrindo temas relacionados a linguagem C# e Programação Orientada a Objetos, APIs REST com ASP NET Core, Microsserviços com ASP NET Core, HTML, CSS e JavaScript, Desenvolvimento Front-end com Angular, Desenvolvimento Front-end com React, JavaScript Intermediário, TypeScript, Formação Arquitetura de Software, Microsoft Azure, Agile, SQL, e muito mais.
Inclui comunidade de centenas de alunos, suporte por ela, plataforma e e-mail, atualizações regulares e muito mais.
Clique aqui para ter mais informações e garantir sua vaga
Conclusão
A maior dificuldade para utilizar TDD é a forma de pensar no desenvolvimento. Ao invés de fazer e testar, o pensamento passa a ser criar um cenário a ser atingido e depois codificar. Nas primeiras vezes, o tempo de codificação será muito superior à abordagem tradicional, mas conforme vai adquirindo experiência, o desenvolvimento fica mais fluido. A vantagem em desenvolver desta forma é que no momento da manutenção, o teste já está pronto, e pode evidenciar possíveis efeitos colaterais, além da qualidade da aplicação ser testada a todo o momento. No entanto, é importante o apoio dos donos do projeto, por que no primeiro momento a entrega de funcionalidades pode demorar um pouco mais do que o desejável, mas isso fará muita diferença no futuro.
Código de exemplo
O código de exemplo está dividido em branches para facilitar a interpretação passo-a-passo do desenvolvimento.
GitHub com os códigos: https://github.com/Imersao-NET-Expert/article-tdd