Banner para o post "Introdução ao JUnit"; é a imagem de um avião em um túnel de vento.

JUnit: primeiros passos com Testes de Unidade

O que são testes de software?

Quando criamos um programa, precisamos ter certeza de que ele funciona adequadamente. Uma das formas de fazer isso é por meio do que chamamos de teste de software. Basicamente, prepara-se uma conjunto de dados para a entrada da aplicação em que se sabe o resultado que ela irá retornar. Assim, ao executá-la, verificamos sua saída e, se ela for igual a esperada, o teste é bem sucedido.

A figura abaixo nos mostra uma visão geral desse processo:

Processo de Teste de Software

Note algumas nomenclaturas comumente usadas:

  • Fixture: uma pré-condição, dados iniciais fixos em que conhecemos qual será o resultado obtido pela aplicação se os fornecemos como entrada.
  • AUT: significa Application Under Test, do inglês, Aplicação Sob Teste.
  • Assertion: são as verificações que fazemos, comparando os resultado obtido com o resultado esperado.

Tipos de Testes

Dependendo de como é feito esse teste, principalmente do que é a “AUT”, os testes podem ser classificados como sendo:

  • Teste de Unidade: a AUT é a menor unidade de software sendo implementada. No caso de Orientação a Objetos, a AUT seria uma classe e testaríamos os seus métodos. Observação: é comum o uso do nome Testes Unitários para se referir aos Testes de Unidade. Eu, particularmente, não gosto desse nome pois parece que haverá apenas um teste.
  • Teste de Integração: a AUT é um componente de software, ou seja, agrega várias unidades para atingir um objetivo/funcionalidade. Neste caso, estamos testando se a integração entre essas diversas unidades que compõem o componente (ou serviço) está funcionando adequadamente.
  • Teste de Aceitação: a AUT é a aplicação como um todo. O teste se baseia em usarmos a interface do programa e verificar se as funcionalidades estão corretas. Observação: é comum o uso do nome Teste Funcional justamente por testar as funcionalidades do programa. Não tenho nada contra esse nome, mas gosto mais do nome cunhado por Kent Back. Os testes de aceitação podem ser, também, de vários outros tipos. Pode-se verificar a performance do programa, sua segurança entre outros critérios.

É importante observar que os Testes de Unidade devem executar muito rapidamente, pois são testados frequentemente. Por isso, eles não devem usar o banco de dados, rede, etc. Senão, seriam, na verdade, Testes de Integração, não é mesmo! 😉

Como Testar?

O nosso foco, neste artigo, são os Testes de Unidade. Vejamos, então, um exemplo de como testar uma classe. Suponha a seguinte unidade de código, a classe Calculadora:

public class Calculadora {

    private double n1, n2;

    public Calculadora(double n1, double n2) {
        this.n1 = n1;
        this.n2 = n2;
    }

    public double getSoma() {
        return n1 + n2;
    }

    public double getSubtracao() {
        return n1 - n2;
    }

    public double getMultiplicacao() {
        return n1 * n2;
    }

    public double getDivisao() {
        return n1 / n2;
    }

}

Nós podemos testá-la de várias formas. Uma delas é criar um programa que lê do usuário os dados de entrada, “exercita” o código da classe Calculadora e o usuário verifica por conta própria se os valores estão corretos. Esse código poderia ser assim:

public class CalculadoraTest {

    public static void main(String[] args) {
        Scanner console = new Scanner(System.in);

        System.out.print("n1: ");
        double n1 = Double.parseDouble(console.nextLine());
        System.out.print("n2: ");
        double n2 = Double.parseDouble(console.nextLine());

        Calculadora aut = new Calculadora(n1, n2);

        System.out.println("Soma.........: " + aut.getSoma());
        System.out.println("Subtração....: " + aut.getSubtracao());
        System.out.println("Multiplicação: " + aut.getMultiplicacao());
        System.out.println("Divisão......: " + aut.getDivisao());
    }

}

Este tipo de teste é chamado “Teste Manual“. Acho que já deu para perceber que, caso haja alguma alteração na classe, o programador vai ter que executar esse programa cada vez que isso ocorrrer e testar, “na mão”, as várias possibilidades. É comum, nesses casos, termos um script de teste. Por exemplo, uma tabela da seguinte forma:

n1 n2 Soma Subtração Multiplicação Divisão
 0  0  0  0  0  NaN
 10  0  10  10  0  Infinity
 0  10  10  -10  0  0
 10  10  20  0  100  1
 10  5  15  5  50  2
 5  10  15  -5  50  0,5

Imagine a cena: o programador imprime essa tabela, executa o programa para cada uma das linhas e vai “ticando” o que deu certo. Obviamente, conforme o número de testes vai aumentando, essa prática se torna inviável (além de ser altamente prejudicial para a saúde mental da pessoa – não, não estou brincando!).

Automatizando os Testes

Um programador mais experiente automatizaria essa tarefa! Ele poderia, por exemplo, criar o seguinte código para testar a classe Calculadora:

public class CalculadoraTest {

    public static void main(String[] args) {
        double[][] fixture = new double[][]{
            {0, 0, 0, 0, 0, Double.NaN},
            {10, 0, 10, 10, 0, Double.POSITIVE_INFINITY},
            {0, 10, 10, -10, 0, 0},
            {10, 10, 20, 0, 100, 1},
            {10, 5, 15, 5, 50, 2},
            {5, 10, 15, -5, 50, 0.5}
        };

        for (int i = 0; i <= fixture.length; i++) {
            double n1 = fixture[i][0];
            double n2 = fixture[i][1];

            System.out.printf("Testando com os números n1 = %f e n2 = %f%n", n1, n2);

            Calculadora aut = new Calculadora(n1, n2);

            double somaObtida = aut.getSoma();
            double somaEsperada = fixture[i][2];
            if (somaObtida == somaEsperada) {
                System.out.printf("Soma OK%n");
            } else {
                System.out.printf("Soma ERRADA! Esperava %f, mas obtive %f%n", somaEsperada, somaObtida);
            }

            double subtracaoObtida = aut.getSubtracao();
            double subtracaoEsperada = fixture[i][3];
            if (subtracaoObtida == subtracaoEsperada) {
                System.out.printf("Subtração OK%n");
            } else {
                System.out.printf("Subtração ERRADA! Esperava %f, mas obtive %f%n", subtracaoEsperada, subtracaoObtida);
            }

            double multiplicacaoObtida = aut.getMultiplicacao();
            double multiplicacaoEsperada = fixture[i][4];
            if (multiplicacaoObtida == multiplicacaoEsperada) {
                System.out.printf("Multiplicação OK%n");
            } else {
                System.out.printf("Multiplicação ERRADA! Esperava %f, mas obtive %f%n", multiplicacaoEsperada, multiplicacaoObtida);
            }

            double divisaoObtida = aut.getDivisao();
            double divisaoEsperada = fixture[i][5];

            if (Double.isNaN(divisaoEsperada)) {
                if (Double.isNaN(divisaoObtida)) {
                    System.out.printf("Divisão OK%n");
                } else {
                    System.out.printf("Divisão ERRADA! Esperava %f, mas obtive %f%n", divisaoEsperada, divisaoObtida);
                }
            } else if (divisaoObtida == divisaoEsperada) {
                System.out.printf("Divisão OK%n");
            } else {
                System.out.printf("Divisão ERRADA! Esperava %f, mas obtive %f%n", divisaoEsperada, divisaoObtida);
            }
        }
    }
}

Com certeza, isso vai poupar muito tempo do programador. Basta ele executar o programa e obter um relatório do que deu errado. O problema é verificar esse relatório toda vez… Veja, um sistema completo não é feito de apenas uma classe. Podem haver milhares delas! Imagine ele ter que executar vários programas de testes e olhar cada um dos relatórios. Sem dúvidas, uma tarefa que também vai se tornando inviável!

JUnit

Além do problema apontado no parágrafo anterior, você deve ter percebido que o programa ficou um tanto quanto complexo e com necessidade de algumas condições para testes específicos (caso do NaN). Esse cenário torna-o de difícil leitura e, consequentemente, de difícil manutenção. Imagine que a classe Calculadora agora tenha um método chamado getPotenciacao(). Para alterar o teste, teríamos que modificar a fixture, acrescentar mais algumas linhas de testes usando o índice correto do vetor. Sem dúvidas, um caminho nada agradável…

É aí que entra o JUnit. Ele é um framework que nos ajuda a criarmos códigos para Testes de Unidade Automatizados de forma que eles não fiquem essa bagunça toda. Além disso, ele emite relatórios de resultados bem fáceis de se visualizar, além das IDEs mais usadas já terem integração, o que facilita a verificação de erros.

Criando Testes com JUnit

Criar testes com o JUnit é muito fácil: basta criar uma classe normalmente e anotar os métodos com @Test. Dentro de cada método de teste, devemos usar os comando assert... para verificar se o resultado obtido está correto.

Não vou entrar em detalhes de como configurar o JUnit sem usar uma IDE. Vamos ver um exemplo usando o NetBeans. Para isso, basta ir no menu “Arquivo” > “Novo Arquivo” e irá aparecer a seguinte tela:

Tela de Novo Arquivo do NetBeans com um Novo Teste de Unidade selecionado.

Nesse caso, já selecionei a Categoria “Testes de Unidade” e Tipo de Arquivo “Teste JUnit”. Basta clicar no botão “Próximo >” que a seguinte tela irá aparecer:

Tela de Criação de um Teste de Unidade no NetBeans

Nela, você irá entrar com o nome da classe de testes (chamei de CalculadoraTest) e o pacote onde ela irá estar (normalmente, no mesmo pacote da classe que você quer testar). No meu caso, eu também deselecionei todos os checkboxes que aparecem. Aconselho que faça isso, pelo menos enquanto está aprendendo a criar suas classes de teste.

Um detalhe sobre o nome da classe de teste: seguindo a convenção usada nas versões anteriores do JUnit, a classe de teste deve ser o nome da classe sendo testada seguida da palavra Test. Como estamos testando a classe Calculadora, o nome da classe que vai testá-la será CalculadoraTest.

Vejamos o código criado pelo NetBeans:

package br.pro.ramon.posts.junit;

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

public class CalculadoraTest {
    
    public CalculadoraTest() {
    }
    
}

Nada demais, certo? E, é para ser simples assim, mesmo!

Testando as Funcionalidades

Agora, vamos acrescentar um método para testar alguma funcionalidade da classe Calculadora. Dica: use um método para testar cada uma das funcionalidades que se quer testar separadamente. Vamos, por exemplo, testar o método getSoma().

public class CalculadoraTest {

    private static final double DELTA = 0.000001;
    
    @Test
    public void testSomaDeveRetornar0QuandoPassamosZeroEZero() {
        double n1 = 0;
        double n2 = 0;

        Calculadora aut = new Calculadora(n1, n2);

        double valorObtido = aut.getSoma();
        double valorEsperado = 0;

        assertEquals(valorEsperado, valorObtido, DELTA);
    }

}

Veja que criei um método chamado testSomaDeveRetornar0QuandoPassamosZeroEZero() anotado com @Test. Eu costumo usar a palavra test na frente de cada método pois era uma convenção necessária nas versões anteriores do JUnit. Mas, fique à vontade para colocar o nome que quiser. Também uso um nome bem descritivo do que o teste faz. Dentro dele, eu preparei a fixture (valores n1 e n2), “exercitei” o código da calculadora e verifiquei usando o assertEquals, que é um método do JUnit para verificar se dois valores são iguais. Nele, passamos o valorEsperado como primeiro parâmetro e o valorObtido como segundo.

No caso de comparação de doubles, é necessário passar um valor que será a máxima diferença que os dois parâmetros anteriores devem ter entre eles para que ainda sejam considerados iguais. Dessa forma, criei uma constante que será usada toda vez que precisarmos fazer a verificação de igualdade entre dois valores double.

Para ver o JUnit em ação, usamos o menu “Executar” > “Testar Arquivo”. O NetBeans irá passar nossa classe de teste para o JUnit que irá criar um relatório de testes. Esse relatório é apresentado pela IDE da seguinte forma:

Relatório de Testes do JUnit

Observe a cor verde, que nos dá um feedback natural de que os testes passaram.

Em caso de erro…

Vamos acrescentar mais um teste:

public class CalculadoraTest {

    private static final double DELTA = 0.000001;
    
    @Test
    public void testSomaDeveRetornar0QuandoPassamosZeroEZero() {
        double n1 = 0;
        double n2 = 0;

        Calculadora aut = new Calculadora(n1, n2);

        double valorObtido = aut.getSoma();
        double valorEsperado = 0;

        assertEquals(valorEsperado, valorObtido, DELTA);
    }

    @Test
    public void testSomaDeveRetornar10QuandoPassamosDezEZero() {
        double n1 = 10;
        double n2 = 0;

        Calculadora aut = new Calculadora(n1, n2);

        double valorObtido = aut.getSoma();
        double valorEsperado = 0;

        assertEquals(valorEsperado, valorObtido, DELTA);
    }

}

Quando executamos o JUnit, obtemos o seguinte relatório:

Relatório de Testes do JUnit com erro

Já a cor vermelha nos dá um feedback natural de que algo deu errado. E, o JUnit nos avisa que, no método testSomaDeveRetornar10QuandoPassamosDezEZero(), ele esperava 0.0, mas o valor recebido foi 10.0. Ou seja, ou a classe Calculadora está errada, ou nosso teste está errado. Se você prestou atenção, viu que eu copiei e colei o código anterior, fiz algumas modificações, mas esqueci de atualizar o valorEsperado. Vamos fazer isso:

public class CalculadoraTest {

    private static final double DELTA = 0.000001;
    
    @Test
    public void testSomaDeveRetornar0QuandoPassamosZeroEZero() {
        double n1 = 0;
        double n2 = 0;

        Calculadora aut = new Calculadora(n1, n2);

        double valorObtido = aut.getSoma();
        double valorEsperado = 0;

        assertEquals(valorEsperado, valorObtido, DELTA);
    }

    @Test
    public void testSomaDeveRetornar10QuandoPassamosDezEZero() {
        double n1 = 10;
        double n2 = 0;

        Calculadora aut = new Calculadora(n1, n2);

        double valorObtido = aut.getSoma();
        double valorEsperado = 10;

        assertEquals(valorEsperado, valorObtido, DELTA);
    }

}

Agora, sim:

Relatório de Testes do JUnit sem o erro

Bom, agora que você pegou o jeito, é só ir escrevendo os testes, um a um. Temos alguma repetição de código (falarei sobre isso daqui a pouco), mas o JUnit tornou os testes bem mais fáceis de serem lidos do que aquele outro código que fizemos para automatizar os testes. Além disso, o relatório é bem melhor! Veja ele completo: 😉

Relatório de Testes do JUnit com os 24 testes propostos

Melhorando nossa classe

Uma coisa que pode estar te incomodando é a questão da divisão por zero. Estamos esperando valores do tipo NaN e Infinity. E, se mudarmos nosso método getDivisao() para verificar se o n2 é zero e gerar uma exceção? Vamos fazer isso:

    public double getDivisao() {
        if (n2 == 0) {
            throw new ArithmeticException();
        }
        return n1 / n2;
    }

Agora, ao executarmos nossos testes, obtermos o seguinte feedback:

Relatório de Testes do JUnit com erros de exception

Olha que bacana! Sabemos exatamente onde estão os erros: nos métodos testDivisaoDeveRetornarInfinityQuandoPassamosDezEZero() e testDivisaoDeveRetornarNaNQuandoPassamosZeroEZero().

Mas, agora, temos um “problema”: como testamos um código que gera exceções? Melhor dizendo, como testamos um código onde sabemos que exceções devem acontecer? Simples:

    @Test(expected = ArithmeticException.class)
    public void testDivisaoDeveRetornarNaNQuandoPassamosZeroEZero() {
        double n1 = 0;
        double n2 = 0;

        Calculadora aut = new Calculadora(n1, n2);

        double valorObtido = aut.getDivisao();
        double valorEsperado = Double.NaN;

        assertEquals(valorEsperado, valorObtido, DELTA);
    }

    @Test(expected = ArithmeticException.class)
    public void testDivisaoDeveRetornarInfinityQuandoPassamosDezEZero() {
        double n1 = 10;
        double n2 = 0;

        Calculadora aut = new Calculadora(n1, n2);

        double valorObtido = aut.getDivisao();
        double valorEsperado = Double.POSITIVE_INFINITY;

        assertEquals(valorEsperado, valorObtido, DELTA);
    }

Notou que podemos passar um expected = ClasseDaExcecao.class como parâmetro da anotação @Test? Isso faz com que o JUnit não dê erro caso a exceção seja gerada e, mais ainda, aponte como sendo uma falha caso ela não seja gerada!!!

Test Driven Development / Test First Design

Nesse exemplo que passei, usamos o método bottom-up, ou seja, criamos primeiro a classe Calculadora para depois criarmos testes para ela. Eu, particularmente, prefiro uma abordagem top-down onde primeiro escrevemos os testes e depois o código para fazer ele passar.

Obviamente, você não conseguirá escrever um monte de testes para um código que não existe. Por isso, a abordagem top-down deve ser feita em pequenos passos, um teste por vez. Quando fazemos isso, estamos usando um método conhecido como Test Driven Development (Desenvolvimento Orientado por Testes). Como essa técnica conduz a códigos mais modulares, ela também é chamada de Test First Design (Projeto baseado Primeiro em Testes). Nota: traduções livres! 😉

Só um detalhe: o TDD envolve mais algumas práticas como, por exemplo, criar o código mais simples que faça o teste passar, baby steps, entre outras. Se quiser ver isso em ação, frequente algum Coding Dojo! 😉

Vejamos nosso caso: para podermos criar um objeto da classe Calculadora precisamos dos números que ele vai calcular. Talvez, se tivéssemos feito um teste primeiro, onde a gente escolhe como seria nossa classe ideal, teríamos feito um código assim:

public class CalculadoraIdealTest {

    private static final double DELTA = 0.000001;

    @Test
    public void testSomaDeveRetornarZeroQuandoPassamosZeroEZero() {
        CalculadoraIdeal aut = new CalculadoraIdeal();
        assertEquals(0, aut.getSoma(0, 0), DELTA);
    }

}

Vamos, então, criar nossa CalculadoraIdeal:

public class CalculadoraIdeal {

    public double getSoma(double n1, double n2) {
        return n1 + n2;
    }

}

E, mais alguns testes:

public class CalculadoraIdealTest {

    private static final double DELTA = 0.000001;

    @Test
    public void testSomaDeveRetornarZeroQuandoPassamosZeroEZero() {
        CalculadoraIdeal aut = new CalculadoraIdeal();
        assertEquals(0, aut.getSoma(0, 0), DELTA);
    }

    public void testSomaDeveRetornar10QuandoPassamosDezEZero() {
        CalculadoraIdeal aut = new CalculadoraIdeal();
        assertEquals(10, aut.getSoma(0, 10), DELTA);
    }

    @Test
    public void testSomaDeveRetornar10QuandoPassamosZeroEDez() {
        CalculadoraIdeal aut = new CalculadoraIdeal();
        assertEquals(10, aut.getSoma(10, 0), DELTA);
    }

    @Test
    public void testSomaDeveRetornar20QuandoPassamosDezEDez() {
        CalculadoraIdeal aut = new CalculadoraIdeal();
        assertEquals(20, aut.getSoma(10, 10), DELTA);
    }

    @Test
    public void testSomaDeveRetornar15QuandoPassamosDezECinco() {
        CalculadoraIdeal aut = new CalculadoraIdeal();
        assertEquals(15, aut.getSoma(10, 5), DELTA);
    }

    @Test
    public void testSomaDeveRetornar15QuandoPassamosCincoEDez() {
        CalculadoraIdeal aut = new CalculadoraIdeal();
        assertEquals(15, aut.getSoma(5, 10), DELTA);
    }

}

Note que, agora, a criação do objeto não depende mais das entradas. Isso permite que a gente possa colocar a criação do objeto em outro lugar, evitando que esse código se repita a cada teste. No entanto, se criarmos o objeto no construtor, ele será o mesmo para cada teste, o que nem sempre é desejável. Existe um princípio de que os Testes de Unidade devem ser independentes entre si. Ou seja, gostaríamos que, a cada método de teste que for executado, um objeto diferente seja criado. Como fazer isso?

Eis que surge a anotação @Before que pode ser usada em um método que será executada antes de cada método de teste. Lá, podemos colocar a criação do objeto aut reduzindo muito nosso código:

public class CalculadoraIdealTest {

    private static final double DELTA = 0.000001;

    private CalculadoraIdeal aut;

    @Before
    public void setUp() {
        aut = new CalculadoraIdeal();
    }

    @Test
    public void testSomaDeveRetornarZeroQuandoPassamosZeroEZero() {
        assertEquals(0, aut.getSoma(0, 0), DELTA);
    }

    public void testSomaDeveRetornar10QuandoPassamosDezEZero() {
        assertEquals(10, aut.getSoma(0, 10), DELTA);
    }

    @Test
    public void testSomaDeveRetornar10QuandoPassamosZeroEDez() {
        assertEquals(10, aut.getSoma(10, 0), DELTA);
    }

    @Test
    public void testSomaDeveRetornar20QuandoPassamosDezEDez() {
        assertEquals(20, aut.getSoma(10, 10), DELTA);
    }

    @Test
    public void testSomaDeveRetornar15QuandoPassamosDezECinco() {
        assertEquals(15, aut.getSoma(10, 5), DELTA);
    }

    @Test
    public void testSomaDeveRetornar15QuandoPassamosCincoEDez() {
        assertEquals(15, aut.getSoma(5, 10), DELTA);
    }

}

Chamei o método anotado com @Before de setUp() porque era a convenção usada para o nome desse método nas versões anteriores do JUnit.

Conclusão

Bom, eu teria mais alguns detalhes para escrever por aqui, mas o post já ficou beeem comprido e, com o que você aprendeu até aqui já dá para testar muuuuuita coisa!!!

Testar software é uma ciência e uma arte, mas espero que, com este artigo, você comece a usar Testes de Unidade Automatizados em seus projetos. Quanto mais testes forem feitos, cobrindo as mais diversas possibilidades de entrada-saída, mais certeza teremos que nosso programa está funcionando corretamente. Caso faça isso, deixe um comentário aqui contando sua experiência com eles.

De qualquer forma, escreva agora um comentário sobre o que achou do texto. Curta o post e compartilhe com outras pessoas. Os botões aí embaixo servem para isso! rsrsrs 🙂

Até breve!

PS: o JUnit, apesar do nome, e de ser normalmente usado para Testes de Unidade, nada impede que ele seja usado também para Testes de Integração e/ou Aceitação. Nesses casos, usam-se frameworks auxiliares como: DbSetup, HtmlUnit, Selenium (tutoriais), entre outros. #ficaadica

PS2: os códigos mostrados aqui podem ser encontrados no GitHub.

Se você gostou do post, tem alguma dúvida ou encontrou algum erro, por favor, deixe uma mensagem! Seu feedback é muito importante!