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



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

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.

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.

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

Criando um jogo de guerra nas estrelas em javascript usando a biblioteca p5.js

Jogo simples de guerra espacial desenvolvido em javascript. Esse jogo usa cálculos de física para simular efeitos de atrito e inércia.

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.

Veja também

Um algoritmo não pode ser composto por instruções ambíguas, isso pode trazer resultados inesperados

Os algoritmos na ciência da computação são o principal meio para o desenvolvedor poder escrever instruções para o computador, operando a sua maneira

A maioria dos sistemas atuais usam a nuvem para o armazenamento e o acesso de dados

A nuvem pode ser uma boa alternativa de substituição da memória atual. Além de fazer a cópia de segurança dos dados, pode ser acessível de qualquer lugar.

Autenticação Biométrica

A autenticação biométrica é o uso de tecnologias que conseguem captar traços e comportamentos únicos de indivíduos para a autenticação.

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.

RFC Request for comments

Documentos com especificações técnicas sobre as tecnologias da internet que são usados para a implementação de novas tecnologias e padronização.

Armazenamento de imagens

O armazenamento de imagens é realizado com o sistema de numeração binária. A imagem é composta por um conjunto de pixels e cada pixel representa uma cor.