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: 1213



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 um jogo de quebra blocos em javascript

Programando um jogo clássico de arcade usando javascript e p5.js. O usuário deve quebrar os blocos utilizando uma bola ao mesmo tempo que evita que a bola saia pela parte inferior da tela

Caixa eletrônico usando arquitetura limpa

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

Tutorial de programação do jogo da serpente em javascript

Programando o clássico jogo da serpente usando o framework p5.js. Tutorial indicado para iniciantes da programação que querem aprender os conceitos básico da área criando jogos.

Implementando um algoritmo de pathfinding

Implementando um programa que encontra a menor distância entre dois pontos dentro de um labirinto usando o algoritmo A* (a-estrela).

Integrando Laravel com o protocolo MQTT para comunicação entre dispositivos

Projeto de comunicação entre dois dispositivos ESP8266 e Raspberrypi4. Laravel irá funcionar como servidor e receptor de dados de temperatura e umidade coletados com o DHT11.

Veja também

A subclasse não deve estender a superclasse que não representa consistência em relação as suas características

No passado existiam girafas com pescoço comprido e as girafas com pescoço curto. Isso resultou em um comportamento inesperado, a seleção natural. Não queremos isso no nosso algoritmo...

O usuário malicioso joga a isca e espera a vítima pacientemente

Phishing tem esse nome pois a vítima se torna só mais um peixe na rede. Ter conhecimento de phishing é o melhor jeito de evitar ser um desses peixes

Variáveis na programação

As variáveis são elementos responsáveis por armazenar informações temporariamente ou perpetuamente durante a execução de um programa ou algoritmo.

Digitalização

A digitalização é a transformação de informação analógica em dados binários, facilitando a cópia, edição e processamento desses dados.

Ativação de produto

O código de ativação de softwares tem como objetivo prevenir a pirataria e revenda de produtos sem o conhecimento e autorização do autor.

Aprendizagem adaptativa

Método de ensino digital que ajusta o nível de dificuldade conforme a capacidade, nível e conhecimento do indivíduo que utiliza o sistema.