Afinal o que é TDD? TDD é a abreviação de Test Driven Development ou, em tradução livre, Desenvolvimento Guiado por Teste. Este artigo apresenta uma introdução ao TDD e ao framework JUnit.

Introdução ao TDD

A ideia por traz do desenvolvimento guiado por teste é que primeiro devemos escrever os testes para posteriormente escrever o código. Segundo Freeman, Steve; Pryce, Nat(2012) “... Você não tem nada a perder, a não ser seus bugs”. Ainda segundo a Wikipédia, “Desenvolvimento dirigido por testes é uma técnica de desenvolvimento de software que baseia em um ciclo curto de repetições: primeiramente o desenvolvedor escreve um caso de teste automatizado que define uma melhoria desejada ou uma nova funcionalidade. Então, é produzido código que possa ser validado pelo teste para posteriormente o código ser refatorado para um código sob padrões aceitáveis”. O TDD é parte do processo de desenvolvimento ágil, utilizado em metodologias como o XP (Programação Extrema) e sendo uma das técnicas que auxiliam na melhoria de qualidade do processo de desenvolvimento. O TDD ou desenvolvimento guiado por teste torna mais eficiente o processo. A imagem a seguir ilustra os passos do desenvolvimento utilizando TDD:

Passos do TDD
Figura 1: Passos do TDD

O desenvolvimento guiado por teste dá uma visão mais ampla do que deve ser feito ao desenvolvedor, pois antes de criar a funcionalidade, deve-se criar um teste da funcionalidade. Os passos apresentados na figura acima indicam que primeiro é necessário criar o teste, este teste é chamado de teste falho, pois a funcionalidade ainda não existe, desta forma, o teste deve retornar um erro. Posteriormente será desenvolvido o código para fazer com que o teste seja bem sucedido, já que o desenvolvedor sabe quais funcionalidades deve implementar, fica mais prático o seu desenvolvimento. Por último refatore, ou seja, melhore a codificação.

O Desenvolvimento guiado por teste é um conjunto de técnicas que culminam em um teste de ponta a ponta. Neste artigo, vamos apenas tratar do teste unitário, que é um dos procedimentos para o TDD, mas deve-se pensar no conjunto, ou seja, o teste de integração e o teste de aceitação.

Teste unitário

O Teste Unitário valida apenas um pedaço do sistema, ou seja, uma unidade, sendo utilizado para testar as funcionalidades, exibindo informações sobre seu funcionamento. Para nosso exemplo prático de codificação, vamos utilizar o JUnit como framework de teste unitário Java, o conceito aqui utilizado pode ser aplicado ao NUnit, PHPUnit, entre outros.

O JUnit é um framework de teste que utiliza anotações para a identificação de métodos de ensaio. Lembrando que os testes são unitários, não devendo depender de outros testes para o seu funcionamento. Além de utilizar anotações, o framework disponibiliza métodos de asserções, que são utilizados para validar informações. Com base nas asserções teremos o resultado de nosso teste como falho ou OK.

Nos exemplos utilizaremos o NetBeans que, ao ser instalado, já possui o JUnit integrado. O Eclipse também o tem integrado, mas é possível fazer o download caso queira fazer o processo manual.

Para nosso exemplo, será criado um novo projeto denominado "Calculo", este projeto será utilizado para criar nosso teste e posteriormente nosso código de aceitação para o teste.

Ao criar o projeto, a seguinte estrutura foi montada:

Estrutura do projeto
Figura 2: Estrutura do projeto

Primeiramente é necessário entender o que vamos testar, visto que criaremos um teste para algo que ainda não existe. Neste caso, o cenário é o seguinte:

Temos de desenvolver uma classe como solução a um problema de cálculo simples, devem ser implementados métodos para soma, subtração, multiplicação e divisão. Nesse nosso exemplo, queremos apenas fazer o cálculo de dois números e obter o resultado.

Agora que já sabemos o que vamos desenvolver, então vamos criar o nosso primeiro teste unitário.

Adicionando novo teste
Figura 3: Adicionando novo teste

Será aberta uma nova janela, conforme a figura a seguir:

Propriedades iniciais do novo teste
Figura 4: Propriedades iniciais do novo teste

O campo "nome da classe" é o nome da classe de teste, vamos dar o nome de "TestCalculo", para sabermos que é um pacote de teste e deve testar a classe Calculo.

Em "código gerado" estão marcadas as opções de inicializador de testes, finalizador etc. Para este exemplo, podemos desmarcar estes itens.

No próximo passo será perguntada a versão do JUnit a ser utilizada, selecione JUnit 4.X.

Versão do JUnit
Figura 5: Versão do JUnit

Crie um Teste JUnit e insira o seguinte código:

import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author Fabio Gomes Rocha
 */
public class TestCalculo {
    
    public TestCalculo() {
   
    }
    @Test
/**
 *
 * @Description Testa o método soma
 */
    
public void testSoma(){
         System.out.println("Soma");
        int a = 10;
        int b = 20;
        int expResult = 30;
        int result = Calculo.Soma(a, b);
        assertEquals(expResult, result);
    }
    @Test
/**
 *
 * @Description Testa o método subtração
 */

       public void testSubtracao(){
         System.out.println("Subtração");
        int a = 20;
        int b = 10;
        int expResult = 10;
        int result = Calculo.Subtracao(a, b);
        assertEquals(expResult, result);
    }
    @Test
/**
 *
 * @Description Testa o método multiplicação
 */

    public void testMultiplicacao(){
         System.out.println("Multiplicação");
        int a = 5;
        int b = 10;
        int expResult = 50;
        int result = Calculo.Multiplicacao(a, b);
        assertEquals(expResult, result);
    }
    @Test
/**
 *
 * @Description Testa o método divisão
 */

    public void testDivisao(){
         System.out.println("Divisão");
        float a = 20;
        float b = 10;
        float expResult = 2;
        float result = Calculo.Divisao(a, b);
        assertEquals(expResult, result,0);
    }
}
Listagem 1. Teste unitário para a classe Calculo

Com o teste pronto, antes de entendermos o que está acontecendo, vamos executá-lo (Shift + F6). Após a execução, o sistema deve retornar que falhou, neste caso, como temos quatro testes, além de falha em 100%, é apresentada a causa do erro. Nesse caso o erro foi ocasionado por falta de implementação da classe Calculo. Um outro ponto importante é que o teste apresenta o tempo que levou para a realização do teste (1,309 segundos) sendo possível analisar sob este ponto para refatorar o teste e a unidade.

Erros no primeiro teste
Figura 6: Erros no primeiro teste

Agora que o teste foi falho, vamos entender a codificação do teste de unidade que desenvolvemos.

  • import org.junit.Test;
  • import static org.junit.Assert.*;

Estão importando as classes necessárias para a realização dos testes e asserções.

Posteriormente, é criado um teste para cada funcionalidade desejada, tomando cuidado com os tipos de retorno a serem testados.

@Teste: Esta anotação indica que o código a seguir é um teste.

public void testSoma(){: Método criado para teste, foi dado o nome test (indicando que é um teste) e Soma (indicando qual o método está sendo testado)

System.out.println("Soma"): Aqui exibe o que está sendo testado, é apenas uma saída comum.

A seguir alguns atributos que serão utilizados como parâmetro para testar nosso objeto calculo

  • int a = 10;
  • int b = 20;
  • int expResult = 30;

int result = Calculo.Soma(a, b): Agora é feito uma chamada ao método soma do objeto calculo, passando como parâmetro as variáveis a e b. Como é uma soma e demos o valor de a como 10 e b como 20, somando teríamos 30.

assertEquals(expResult, result): O assertEquals compara duas variáveis, neste caso o expResult(Resultado esperado) com a variável result que traz o resultado obtido pelo método soma do objeto calculo. Nesse caso se o resultado for 30 então o teste foi bem sucedido.

Teste aprovado
Figura 7: Teste aprovado

Na imagem anterior é possível ver o teste sendo realizado, que aparece como aprovado. Isto é possível depois de ter implementado a classe Calculo, que está abaixo, caso contrário, erros ocorreriam. Se fizermos modificações propositais para verificar se está OK, podemos obter erro, por exemplo, a soma de 10 mais 20 sendo igual a 40.

Passos aprovados e um com erro
Figura 8: Passos aprovados e um com erro

Aqui temos três testes como aprovados e um como incorreto, ou seja, 75% dos testes foram bem sucedidos.

Aqui temos a classe Calculo, desenvolvida de forma simples, apenas para a implementação das funcionalidades desejadas. Se a codificação possuir algum erro de funcionalidade, o JUnit vai exibir uma mensagem de erro.

public class Calculo {
      public static int Soma(int a, int b){
        int res=a+b;
        return res;
    }

    static int Subtracao(int a, int b) {
         int res=a-b;
        return res;
    }

    static int Multiplicacao(int a, int b) {
         int res=a*b;
        return res;
    }
    static float Divisao(float a, float b) {
         float res=a/b;
        return res;
    }    
}
Listagem 2. Classe calculo

Anotações do JUnit 4.x

As anotações indicam a funcionalidade do método para o framework, segue uma lista sucinta das anotações do JUnit 4.

@Teste: Identifica que o método é um teste;

@Before: Indica que o método deve ser executado antes do teste, pode ser utilizado para preparar o ambiente de teste;

@After: Indica que o método deve ser executado depois de cada teste, pode ser utilizado para limpar o ambiente de teste;

@BeforeClass: Indica que o método será executado antes do início do ensaio;

@AfterClass: Indica que o método será executado ao finalizar o ensaio.

Assert do JUnit 4.x

As asserções são métodos de ensaio que possibilitam a validação do teste. A lista a seguir contem algumas asserções.

fail(String): Deixa o método falhar, normalmente utilizado para verificar se uma determinada parte do código não esta sendo atingido;

assertTrue(true)/(false): Será sempre verdadeiro ou falso, pode ser utilizado para pré-definir um resultado de um teste, se o teste ainda não foi implementado;

assertEquals(mensagem, esperado, real): Testa dois valores verificando se são iguais.

assertEquals(mensagem, esperado, tolerância, real): Teste de valores do tipo double ou float, a tolerância, neste caso, é o numero de casas decimais que deve ser validado;

assertNull(mensagem, objeto): Verifica se o objeto é nulo;

assertNotNull(mensagem, objeto): Verifica se o objeto não é nulo;

assertSame(String, esperado, real): Verifica se as duas variáveis se referem ao mesmo objeto;

assertNotSame(String, esperado, real): Verifica se as duas variáveis se refere a objetos diferentes;

Com base nas asserções acima, é possível criar diversos testes com o JUnit.

Benefícios em iniciar o desenvolvimento pelo teste

Em primeiro lugar, quando é criado o teste, normalmente quer dizer que foi entendido o que é para se desenvolver. No exemplo anterior, queremos um objeto que tenha métodos para realizar soma de dois números, multiplicação, etc. Então podemos testar os retornos, garantindo a qualidade da funcionalidade. Outro beneficio é a redução de problemas, pois já foram testadas as unidades do sistema. Claro que temos que testar ainda a integração e a aceitação, mas o primeiro passo foi dado para o sucesso do produto.

Alem disso, é apresentado o tempo que levou para serem realizados os testes, desta forma, é possível ainda fazer uma análise para a refatoração, ou seja, o processo de melhoria da unidade através de melhor codificação.

Conclusões

Esta foi a uma breve introdução ao TDD e ao JUnit buscando tratar as informações de forma prática e simples.

Lembre-se: o desenvolvimento guiado por teste é um dos fatores de sucesso na criação de sistemas sustentáveis e com baixa incidência de problemas.

Veni, vidi, codi (Vim, vi e codifiquei)

Referências