Se você nunca ouviu falar de TDD, ou se já ouviu mas ainda não se arriscou, está aí uma grande oportunidade de entrar de cabeça nessa abordagem prática, voltada a processos de fluxos cíclicos e constantes. Convidamos mais uma vez, o André Zarlenga, um de nossos consultores e desenvolvedor aqui na Platform Builders que preparou um super artigo sobre TDD


Introdução

Sempre que penso sobre TDD (Test Driven Development – Desenvolvimento Orientado por Testes), me vem à mente o conceito de qualidade de software. Que resulta em um código mais claro, simples e sem erros, ou quase.

Penso que um bug que chega nas mãos do usuário, pode ocorrer somente uma vez, este deve ser entendido, e corrigido, digo isso por que temos que considerar que somos humanos falhos e que não conseguimos prever 100% dos cenários possíveis.

Motivação

Antes de mais nada, vamos abordar o porquê de devermos levar em consideração o teste do código, já que testar leva mais tempo para entregar o software, módulo, micro serviço, etc, ou seja, a qualidade que o teste nos dá tem um custo, já que se trata de maior tempo de desenvolvimento do código e teste.

Vamos para uma analogia, imagine que você leitor é responsável por construir pontos de ônibus. Você ergue duas colunas e um teto, e está entregue, está ok, sempre funciona, agora vamos para um situação hipotética.

Certa vez o ponto desabou, causando um acidente para quem ali estava. Levando o responsável pelo ponto de ônibus ao tribunal, ou seja, são custos com o processo, danos, etc.
Isso nos faz pensar que seria mais barato manter um sistema de segurança, além das colunas, que custaria mais em material e projeto, entretanto sairia mais barato para ambos os lados ao longo prazo. É claro que isso é apenas um exemplo simplório da realidade, mas com fim didático.

O custo mencionado no exemplo acima, tem sido percebido principalmente pelo mercado financeiro, onde uma manutenção ou adição de nova funcionalidade no software, pode trazer um efeito colateral não previsto, com prejuízo financeiro. Eles perceberam que vale a pena gastar um pouco mais de tempo, entregando uma solução mais assertiva com uma perspectiva de investimento e não apenas de custo.

Primeira abordagem

Uma coisa que deve ficar claro sobre como abordar um teste, é objetivar-se no negócio e não exatamente no código. Eu vejo muito material sobre testes de software, que levam um livro inteiro abordando apenas como usar a ferramenta e muito pouco ou nada de estratégia de como testar, digo isso, porque geralmente o uso do framework é muito simples, se trata basicamente de um ponto de entrada e verificação de saída. Os testes devem trazer relevância ao propósito do algoritmo, com conceitos bem difundidos do ponto de vista estratégico principalmente na geração de valor.

O TDD vem nos ajudando muito nesse processo de evolução de software, ele nos faz escrever um novo código, caso o teste automatizado falhe. Vou abordar aqui o ciclo que o TDD propõe, não vou me ater às estratégias neste artigo de adoção do teste para o negócio.

O que posso dizer é que para saber o que testar, é necessário conhecer o negócio, visto que vejo muitas vezes o desenvolvedor confuso sobre qual abordagem tomar para o teste.

Um ponto que acho muito interessante, é o modo como o TDD faz com que a percepção de teste mude naturalmente. Conforme você vai testando, vai percebendo gaps nos testes e modificando os mesmos e códigos alvos em consequência. Segue um exemplo abaixo.

Ciclo do teste

O conceito simples do TDD é escrever e corrigir os testes com falha antes de escrever o novo código (antes do desenvolvimento).

Isso ajuda a evitar a duplicação de código, à medida que escrevemos uma pequena quantidade de códigos, por vez, para passar nos testes. (Os testes são apenas condições de requisitos que precisamos testar para cumpri-los).

Quais passos você deve executar:

Crie o teste
Execute os testes criados anteriormente
Escreva o algoritmo
Execute os testes novamente
Repita os passos

Fluxo Cíclico TDD

  Fluxo de desenvolvimento com TDD

No primeiro passo o que costumo fazer é criar a classe de teste, com a premissa daquilo que pretendo testar em mente. Costumo criar uma lista de testes, com descrição do mesmo.

Aqui vamos abordar sobre teste unitário, porém vamos criar um post sobre teste de integração, mostrando todo seu valor.

Pia

Ambos funcionam – mas no conjunto…

 



Para cada um deles uso a estratégia do AAA (Arrange, Action e Asserts), que sugere uma divisão de método em três seções.

Arrange 

Na primeira seção (Arrange), devemos criar as instâncias de objetos, os quais são alvo de teste. Quando criamos nossa instância, percebemos que em algumas ocasiões temos dependências que não fazem sentido naquele contexto, como uma classe que depende de uma interface, que é responsável por executar operações em banco de dados por exemplo.

Se estivermos nos testes de unidade, não devemos fazer chamadas externas, como banco de dados, acesso às APIs, internet, etc. Como o nome evidência, devemos enfocar na unidade. Temos dois possíveis cenário:

Nossa classe já tem conhecimento que depende de uma conexão com banco de dados. Neste caso, vamos “enganar” nossa classe passando um objeto que não vai executar operações em nosso banco de dados, de fato, nosso algoritmo de funcionamento, pensará que sim.

Imagine que estamos testando um cadastro de cliente, onde este irá ser persistido no banco de dados. Porém o algoritmo não é responsável apenas por cadastrar, mas também pela responsabilidade de verificar se o usuário “cadastrante” tem permissão para cadastrar um novo usuário. Neste caso, sabemos que há uma instância de usuário e também uma verificação envolvida.

Com esse cenário, também descobrimos que temos mais de um caso de teste. Onde o usuário “cadastrante” tem permissão para cadastrar, é um caso de teste, para o usuário que não o tem.

A maioria dos frameworks de teste, disponibilizam um meio fácil de reaproveitar o algoritmo através de annotations, alguns desenvolvedores dizem que não é bom utilizá-los, afinal o método deve ser duplicado quantas vezes for necessário.

Parto do princípio que manter o código reutilizável e também o algoritmo de teste deve ser feito com organização e coesão, afinal de contas, ele tem que ser mantido ao longo do projeto e quanto mais duplicação, maior será o trabalho para manutenção.

De volta ao ponto principal, estou me referindo ao famoso Mock, que no linguajar do desenvolvedor significa “mockar” um objeto para enganar o algoritmo. No exemplo acima, devemos enganar nossa interface de banco de dados, para que, quando o algoritmo invocar a persistência de fato, ele ache que foi pro banco de dados, e que tenha um retorno adequado; Considerando que você espera do teste falha ou sucesso.

Quer dizer que devemos testar a falha também?

Com certeza, devemos testar o caminho feliz e ao qual esperamos um exceção.

Leia também: O Que Não Te Contaram Sobre Os Microsserviços

Action

Nesta etapa, temos as chamadas do alvo que queremos testar. Em outras palavras, faremos o teste de unidade real e o resultado que será obtido. Basicamente, chamaremos o método de destino, fazendo alusão a instância criada na etapa anterior.

Um ponto interessante neste passo, é que percebemos naturalmente como devemos separar mais nosso algoritmo. Caímos em situações que desejamos testar um ponto bem unitário e isolado, e nos damos conta de quanto estamos acoplados a outros fatores.

Exemplificando: Vamos imaginar que temos um método que controla a baixa de estoque, porém esta deve ser realizada no momento que o pedido esteja com o status pago. Podemos perceber como temos um conjunto de regras, afinal a ideia é entendermos o que desejamos testar.

Devemos criar um teste que garanta que o método só realize a baixa de estoque, caso o status esteja como pago. Neste primeiro teste devemos testar o caminho feliz e o adverso, ou seja, devemos esperar em nosso teste que o estoque realize a baixa, quando definimos o status como pago.

E não podemos nos esquecer de testar o que ocorre quando o status não está como pago, dessa forma, iremos garantir futuras alterações neste algoritmo, que irão ficar seguras nesse ângulo de teste.

No exemplo acima, percebemos que para testar a baixa no estoque, não é possível executá-la em si, devemos ter condição de testar a realização da baixa isoladamente, tendo em vista que ela executa um cálculo de subtração onde nunca deve ficar negativo, ao menos nesse exemplo (já vi casos onde a regra poderia ficar negativa e não há problema nisso, desde de que, esteja respeitando a regra do negócio).

Asserts

Nesta etapa, executamos o teste com os resultados dos métodos utilizados na etapa anterior, repare que digo no plural, porque podemos executar algumas chamadas de métodos, porém estes devem fazer sentido para seu teste. Você não vai querer colocar todos seus testes em um só método.

Aqui podemos testar não somente valores esperados, como também podemos esperar exceções. Vejo isso como saudável, desde de que, esse comportamento, esteja nos planos de teste.

Por exemplo, imaginemos um pedido de um cliente, isso é uma regra que não pode ser violada, neste caso, entendemos que isso nunca pode ocorrer. Em nosso método alvo, podemos fazer uma verificação, caso o cliente não esteja definido, lançamos uma exceção informando que não deveria ocorrer em tempo de execução.

Esse tipo de tratamento, não é direcionado para o usuário final, mas sim para garantir que ninguém no futuro coloque uma regra burlando isso, ou seja, é feita para proteger o software do próprio desenvolvedor, no presente ou no futuro.

As regras que mencionei são rasas, são apenas para fim didático.

Construção do teste

Vamos ao cenário hipotético criado do zero, no campo teórico. Para aplicar em alguma linguagem. Vamos especificar as regras para uma conta corrente:

Conta corrente deve ter um número de conta e um saldo inicial
O número de conta deve ter um padrão no seguinte formato: 9999-9 (9 corresponde ao digito)
O Saldo inicial da conta corrente deve ser superior ou igual há 50 reais
O correntista deve conseguir sacar o dinheiro de sua conta corrente
Ao sacar o saldo deve ser subtraído do valor do saque
Não deve ser possível realizar saque com valor negativo
O correntista deve ter possibilidade de depositar dinheiro em sua conta corrente
Não deve ser permitido realizar depósito em conta corrente com valor negativo.

Temos acima algumas regras para aplicar, o primeiro passo é definirmos as características e comportamentos de nossa classe correspondente a conta corrente.

Criei um projeto baseando-me em alguns exemplos, separei os testes em alguns arquivos para ficar mais didático, conforme escrevo. Do contrário, colocar os testes na mesma classe, uma vez que se trata do mesmo contexto.

Conta corrente deve ter um número de conta e um saldo inicial

Vamos ao nosso primeiro teste, contudo não temos classe de conta corrente ainda!

Ai que está um ponto do TDD, começamos pelo teste, mas porque isso acontece?

Pensando no teste, vamos imaginar como utilizar a classe e fazer um ciclo de criação do mesmo. Podemos perceber de imediato que nossa classe tem uma dependência de como deve ser construída, onde é exigido saldo inicial e identificador da classe, ou seja, não podemos dar condições de criar uma classe que seja inconsistente, perante nossas regras. Vamos ao teste.

Nosso arrange deve ter uma definição de identificação da conta e saldo. Em seguida, devemos criar a instância de conta corrente, percebemos como nossa classe deve ser construída (adiantei esse ponto quando mencionei identificação da conta corrente e saldo).

Vamos começar pelo mais simples. Devemos estabelecer uma classe com uma propriedade para definir um identificador da conta corrente e outra para armazenar o saldo que conseguir trabalhar com casas decimais, considerando os centavos.

Na prática devemos ter uma classe com um construtor que pede como argumento, saldo inicial e identificador da conta corrente. Então, já sabemos como criar nossa classe. Um detalhe importante nisso, é que não deve ser possível alterar o saldo ou a conta corrente através dos meios que definimos. Assim garantimos a consistência da regra.

 Tudo que você precisa saber sobre o TDD | Diagrama de classe, notação UML - Conta corrente

Diagrama de classe, notação UML – Conta corrente

Vou deixar aqui um exemplo no GitHub, em C# da evolução dos testes. Mas o princípio é o mesmo para outras linguagens.

 Tudo que você precisa saber sobre o TDD | Resultados dos testes

Resultados dos testes

Temos nossa classe de conta corrente e nosso primeiro teste. Este testou o caminho feliz.

  1.  Onde uma instância não pode ser null, ou seja, deve ser criada.
  2. Verificamos se o saldo inicial é o mesmo informado no construtor
  3. Verificamos se a identificação da conta corrente é igual ao informado na criação da instância.

O número de conta deve ter um padrão no seguinte formato: 9999-9 (9 corresponde ao digito)

Nos próximos testes, vamos adicionar uma validação para garantir que o formato da identificação da conta corrente esteja correta e caso não seja uma exceção deve ser lançada. Um detalhe importante, é que temos mais de um caso de teste. O saldo da conta neste caso é indiferente, já que nosso teste, é voltado para identificar o formato apenas.

Resultado dos testes...

Resultado dos testes…                                          

Este teste realizou:

1 – Tentou criar uma conta corrente com uma identificação incorreta:
1-5
1234-51
1234-
1234
1
VAZIO
SEM VALOR

Todos esse casos de teste, devem receber uma exceção, e como nosso teste prevê que isso deve ocorrer, logo eles devem passar.

O Saldo inicial da conta corrente deve ser superior ou igual há 50 reais

Neste próximo teste devemos validar se o saldo é igual ou maior que 50 reais, do contrário uma exceção deve ser lançada. Isso está previsto no teste.

 

 Tudo que você precisa saber sobre o TDD | Resultados dos testes

Resultados dos testes

 

Neste teste o identificador da conta corrente está fixo, isso é válido já que nosso objetivo é o teste sobre o saldo.

O correntista deve conseguir sacar o dinheiro de sua conta corrente – caminho feliz

Neste caso mudamos um pouco a estrutura, onde nosso arrange passou a ser a conta corrente, o que desejamos testar, é o saque com valor inferior ou igual ao saldo. Estamos testando o caminho feliz, onde temos um saldo de 200 reais, e fazemos alguns casos de teste com saques de:

  • 20
  • 200
  • 199

Uma observação importante é que a cada caso de teste temos uma conta corrente com 200 reais, por isso é possível realizar saque com esses valores, portanto poderíamos testar de forma acumulativa também, executando de maneira separada.

 Tudo que você precisa saber sobre o TDD | Resultados dos testes

Resultados dos testes

 

O correntista deve conseguir sacar o dinheiro de sua conta corrente – com exceção

Também temos que testar a situação onde uma tentativa de sacar um valor negativo e superior ao saldo disponível.
 Tudo que você precisa saber sobre o TDD | Resultados dos testes

Resultados dos testes


Neste teste, tentamos sacar:

  • -10
  •  201
  • 542

Nenhum destes valores pode passar sem lançar uma exceção.

Ao sacar o saldo deve ser subtraído do valor do saque – caminho feliz.

Neste próximo teste, vamos verificar o saldo restante, após um saque. No exemplo do GitHub que disponibilizei, agora tem dois parâmetros:

Valor do saque
Valor do saldo após o saque

 Tudo que você precisa saber sobre o TDD | Resultados dos testes

Resultados dos testes

O correntista deve ter possibilidade de depositar dinheiro em sua conta corrente

Neste teste experimentamos o depósito com valores válidos, e verificamos o saldo após

Resultados dos testes

Resultados dos testes

 

Não deve ser permitido realizar depósito em conta corrente com valor negativo

Resultados dos testes

Resultados dos testes

Vamos testar um depósito com valor não permitido, onde uma exceção deve ser lançada.                                              

Desafio

Depois de criar nossa implementação para conta corrente, imagine que seja necessário adicionar descontar uma taxa para o banco, ao depositar um valor. Mesmo sem mexer em nada, já sabemos que alguns testes irão quebrar, porque o saldo não será o mesmo que estava previsto em nossa regra, os requisitos mudam, e isso não é errado, vai de acordo com a necessidade do momento, logo, os testes estão aí para nos mostrar as consequências de uma “simples” alteração.

Conclusão

O TDD vai além de testar, envolve qualidade de código, você vai evoluindo as classes envolvidas, e percebendo naturalmente a necessidade de adotar algumas premissas para um código de qualidade. Uma delas é o SOLID, sobre o qual pretendo escrever no próximo artigo.

Veja o projeto completo no GitHub