TDD Desenvolvimento orientado por testes

Método de desenvolvimento em que os testes são a base da implementação. Eficiente para mitigar bugs de forma automática.

Categoria de Estratégia

Postado em 14 março 2024

Atualizado em 14 março 2024

Palavras-chave: tdd,testes,programacao,java,desenvolvimento,tecnologia

Visualizações: 783



O funcionamento de um software pode ser afetado negativamente após o lançamento de uma nova versão. Por mais que um software tenha uma estrutura de código eficiente, ele continua sendo suscetível a bugs gerados em novas versões. Grande parte dos bugs podem ser descobertos com testes e reparados antes do lançamento da nova versão, diminuindo ou até anulando a existência de bugs. Entretanto, a alteração de código existente pode trazer grandes problemas, pois isso afeta funcionalidades que já estão em produção. Aplicando o princípio aberto/fechado pode acabar resolvendo esse problema, porém nem sempre isso é possível.

Queremos um método que nos de certeza de que todas as funcionalidades do software continuem funcionando exatamente como estavam antes do lançamento de uma nova versão. Ou seja, um método que permita alterações de grande escala no código sem interferir no que sempre funcionou. Esse método existe e se chama TDD.

O que é o TDD (test-driven development)?

O TDD é um método de desenvolvimento baseado em testes. Os testes são escritos em código e cobrem todos os cenários possíveis de uma funcionalidade. Toda funcionalidade deve ter testes e todo teste deve ser independente de outros testes.

Dado os valores das variáveis necessárias para a inicialização de testes, o resultado esperado já é definido pelo criador do teste que sabe exatamente qual valor deve ser retornado no final. A regra é simples, se o resultado retornado não é o esperado, o teste falha, caso contrário, o teste passa.

O grande diferencial do TDD para os outros métodos de teste é a ordem em que os testes são elaborados. No TDD o teste é escrito antes mesmo da implementação da própria funcionalidade. Ou seja, um teste é criado com classes e métodos que ainda não existem. Isso quer dizer que a primeira vez que o teste é executado, ele deve falhar. Esse é o modo como o TDD é aplicado. A aplicação do TDD tem 3 etapas a serem seguidas:

  1. Falhar
  2. Fazer o teste passar
  3. Refatorar

As etapas devem sempre seguir o mesmo sentido. O número de iterações não é importante desde que o objetivo seja alcançado: código testável e eficiente.

tdd teste guiado por testes

Como montar os testes antes da implementação usando TDD?

A grande dúvida é como os testes podem ser escritos sendo que a implementação do código ainda nem foi realizada. Isso é um dúvida bastante comum na primeira vez que alguém conhece o TDD, pois na maioria das vezes os testes são escritos depois da implementação. Isso acaba fazendo com que criemos testes voltados para a implementação. No TDD criamos implementação voltada para os testes. É ao contrário.

Para que o TDD seja aplicado com eficiência, é necessário o desenvolvedor saber com antecedência qual comportamentos ele espera e quais cenários ele deve testar. Por exemplo, em um programa de estacionamento, onde deve-se verificar um carro prestes a estacionar, sabemos que temos que testar os seguintes cenários:

  • Se a vaga estiver disponível, o carro pode estacionar
  • Se a vaga estiver indisponível, o carro não pode estacionar
  • Se um mesmo carro estiver em duas vagas ao mesmo tempo, deve-se manter apenas o último evento (cenário pessimista)

Já sabemos quais são os cenários, por isso podemos criar três testes para essa funcionalidade. Após criar os testes, executamos e vemos as 3 falhas acontecerem, pois não existe implementação ainda. Queremos ter certeza que testamos todos os cenários possíveis e a implementação lide com todos eles.

Como funciona o TDD?

Os testes devem cobrir todos os comportamentos esperados de cada caso de uso. Um caso de uso pode ser uma função pública que comporta algumas regras de negócio. Por exemplo, se temos uma calculadora, ela terá os seguintes casos de uso:

  • Somar
  • Subtrair
  • Multiplicar
  • Dividir

Cada item acima pode ser considerado um caso de uso no caso de uma calculadora. Cada caso de uso deve ter vários testes testando todos os cenários possíveis, sejam cenários positivos ou pessimistas. Por exemplo, o caso de uso “somar” deve ter os seguintes cenários:

  • 1 + 1 é igual a 2
  • -1 + -1 é igual a -2
  • Lançar uma excessão caso o resultado da soma seja INT_MAX (valor máximo que um int pode ter)
  • Lançar uma excessão caso o resultado da soma seja INT_MIN (valor mínimo que um int pode ter)

Os 4 cenários acima não passam de alguns exemplos. Na prática poderiam ter mais cenários. O objetivo é fazer com que a implementação seja alinhada com os testes para nos certificarmos de que implementamos de um modo que atenda cada cenário.

Existem casos em que queremos testar os comportamentos de uma funcionalidade que depende de outro software externo (ex: microserviço). Por ser um software externo e não termos controle sobre o seu comportamento, devemos simular o comportamento desse software externo para que possamos testar como vai ser a reação do nosso software local. O Mockito é um bom exemplo de biblioteca que simula comportamentos de classes e métodos.

Vantagens em aplicar o TDD

A grande vantagem em aplicar o TDD é a garantia de que o software não irá falhar por um mesmo erro duas vezes. Ao encontrar um bug no software, criamos um teste para verificar o comportamento esperado de uma funcionalidade sem o bug. Após criar o teste, arruma-se o bug e o teste deve passar. O teste que cobre esse bug continuará existindo e será executado toda vez que os testes forem rodados. Isso nos dá a garantia de que nunca mais nos deparamos com esse tipo de bug. Caso o mesmo bug venha a acontecer, esse teste irá falhar.

Além disso, se os testes estiverem cobrindo todos os cenários possíveis com eficiência, dificilmente o software irá quebrar. Conforme novas funcionalidades vão sendo implementadas, mais testes serão adicionados para cobrir cada cenário possível. A desvantagem do TDD é o tempo investido necessário para escrever os testes. Por essa razão, alguns optam por não adotar essa método. Entretanto, ao longo prazo, o TDD evita dores de cabeça devido a bugs que poderiam ser evitados com cobertura de testes.

Exemplo de implementação

Imagine um programa de pagamento onde o usuário deve gerar um boleto. Se o boleto for gerado com sucesso ele deve ser persistido no banco de dados. O código identificador do boleto é gerado separadamente e passado posteriormente para esse método de geração. Os testes devem ser escritos da seguinte forma:

public PaymentslipPaymentTest {
    PaymentslipPayment paymentslipPayment;
    PaymentslipRepository paymentslipRepository;
    
    @Test
    public void generate_must_generate_and_persist_paymentslip() {
        String testCode = "12345";
        paymentslipPayment.generate(testCode);
        
        List<Paymentslip> paymentslips = paymentslipRepository.findAll();
        assertEquals(1, paymentslips.size());
        assertEquals(testCode, paymentslips.get(0).getCode());
    }
    
    @Test
    public void generate_must_throw_when_code_is_null() {
        String testCode = null;
        assertThrows(Exception.class, () -> paymentslipPayment.generate(testCode));
        
        List<Paymentslip> paymentslips = paymentslipRepository.findAll();
        assertEquals(0, paymentslips.size());
    }
}

Os testes irão falhar pois o método generate ainda não foi implementado. Passamos para a próxima etapa que é fazer o teste passar.

public PaymentslipPayment {
    private PaymentslipGenerator paymentslipGenerator;
    private PaymentslipRepository paymentslipRepository;
    
    public void generate() {
        Optional<Paymentslip> paymentslip = paymentslipGenerator.generatePaymentslip();
        if (!paymentslip.isPresent()) {
            throw new Exception("Could generate paymentslip")
        }
        
        paymentslipRepository.save(paymentslip.get());
    }
}

O usuário deve usar o método generate para gerar o boleto. O gerador de boleto irá retornar um valor opcional que caso não exista irá lançar um exceção. Caso não sejam encontrados problemas, o boleto gerado é salvo no banco de dados através do repositório.

Esse é um exemplo básico de como pode ser aplicado o TDD. Nesse exemplo, ainda existem mais cenários que podem ser cobertos. Como por exemplo quando a código do boleto é uma String vazia ou quando um boleto repetido é gerado.

Conclusão

O TDD é uma garantia de que o software continuará funcionando mesmo após o código sofrer alteração. Sua grande vantagem é que ele é capaz de diminuir drasticamente o número de bugs que podem vir a acontecer. Um detalhe importante é que quando o TDD é aplicado em um projeto maduro é quase impossível fazer com que o código seja testável. Essa é a importância de aplicar primeiro o teste e depois a implementação.

Projetos práticos

Desenvolvendo o campo de visão de um personagem em um plano 2D

Detectando objetos que entram dentro do campo de visão do personagem. Útil para servir de "gatilho" para eventos em um jogo.

Caixa eletrônico usando arquitetura limpa

Usando JavaFX e arquitetura limpa para criar um aplicativo de caixa eletrônico extremamente simples.

Usando dados fornecidos pelo TSE para simular o gráfico das eleições presidenciais de 2022

Simulação dos gráficos do segundo turno das eleições presidenciais, utilizando python e ferramentas de análise de dados, pandas e jupyter.

Integrando o PHP com Elasticsearch no desenvolvimento de um sistema de busca

Projeto de criação de um sistema de busca usando o framework Symfony e Elasticsearch. A integração com Kibana também é feito de modo remoto com um raspberrypi.

Criando o esqueleto de um jogo de tiro 2D visto de cima usando P5.js

Usando lógicas matemáticas como trigonometria para criar e calcular o esqueleto de um jogo de tiro 2D em javascript

Veja também

Pessoas sem um endereço não podem utilizar os correios. Dispositivos sem um endereço não podem acessar a internet.

Quando nos conectamos à internet, nós recebemos um endereço IP. O endereço IP é o nosso endereço virtual que vai servir como localização para a transferência de dados na internet

Uma das vantagens de ter um robô trabalhando para você é o aumento da produtividade

Hoje em dia não é mais necessário gastar o nosso tempo com tarefas que podem se automatizadas. Os robôs estão aqui para nos ajudar...

Modelo cascata

Modelo tradicional de desenvolvimento de software, cada etapa do projeto deve seguir a ordem dos processos sem retroceder para as etapas anteriores.

Vetores geométricos

Caracterizam uma grandeza física que possui módulo, direção e sentido. Pode simular eventos como queda, atração e deslocamento de objetos em um meio.

Websockets

Protocolo que atua sobre o protocolo HTTP para múltiplas transferências de dados com uma única conexão com o intuito enviar e receber dados em tempo real.

Algoritmo

O algoritmo é um conjunto de instruções escritas por um programador com intuito de solucionar um problema ou obter um resultado previsto.