PROGRAMAÇÃO COM OBJECTOS 2010/2011

Uma Introdução ao JUnit (3.8)

O Junit é um framework de testes escrito por Erich Gamma e Kent Beck que facilita a implementação de unidades de teste em Java. JUnit é open source e oferece um conjunto de classes permitindo a fácil integração e execução regular de testes durante o processo de desenvolvimento, de forma a ter uma medida mais concreta do seu progresso.

Em http://www.junit.org/, poderá fazer o download da versão mais recente de JUnit. Após guardar o ficheiro para o seu disco, descomprima-o numa directoria à sua escolha e está concluída a instalação. Eventualmente, poderá ser conveniente incluir o ficheiro junit.jar na variável de ambiente classpath.

Imagine que pretende representar uma classe que defina o funcionamento das portas. Uma porta pode ser aberta, fechada, trancada ou destrancada. Não é possível abrir uma porta que esteja trancada nem trancar uma porta que esteja aberta. Tentativas deste tipo não alterarão o estado da porta. Acções redundantes (tais como tentar abrir uma porta já aberta, tentar trancar uma porta já trancada, etc.) também não alterarão o estado da porta. Escreva, no ficheiro Porta.java (assumido na directoria c:\javacode\porta\), a seguinte classe:

public class Porta {
    private boolean _fechada = true;
    private boolean _trancada = true;

    public boolean estaAberta() {
        return !_fechada;
    }

    public void abrir() {
        if(!_trancada)
            _fechada = false;
    }

    public void fechar() {
        _fechada = true;
    }

    public void destrancar() {
        _trancada = false;
    }

    public void trancar() {
        if(_fechada)
            _trancada = true;
    }
}

Agora convém testar se a classe que escrevemos está a funcionar correctamente. Para criar uma unidade de testes através do JUnit basta criar uma subclasse de TestCase. Vamos chamar-lhe TestePorta. Esta classe vai testar se o comportamento de uma porta é o pretendido. Escreva, no ficheiro TestePorta.java (assumido na directoria c:\javacode\porta\), o seguinte código:

import junit.framework.TestCase;

public class TestePorta extends TestCase {

    private Porta _portaCobaia = new Porta();

    public static void main(String[] args) {
        junit.textui.TestRunner.run(TestePorta.class);
    } 

    public void setUp() {
        _portaCobaia.destrancar();
        _portaCobaia.abrir();
    }

    public void testAbrirFechar() {
        assertTrue("A porta devia estar inicialmente aberta",
                        _portaCobaia.estaAberta());
        assertEquals("A porta devia estar inicialmente aberta",
                            true, _portaCobaia.estaAberta());
        _portaCobaia.fechar();
        assertFalse("Ao fechar uma porta aberta esta continuou aberta",
                        _portaCobaia.estaAberta());
        _portaCobaia.fechar();
        assertFalse("Ao fechar duas vezes a porta esta ficou aberta",
                        _portaCobaia.estaAberta());
        _portaCobaia.abrir();
        assertTrue("Ao abrir uma porta apenas fechada esta continuou fechada",
                        _portaCobaia.estaAberta()); 
        _portaCobaia.abrir();
        assertTrue("Ao abrir duas vezes a porta esta ficou fechada",
                        _portaCobaia.estaAberta()); 
    }

    public void testTrancarDestrancar() {
        assertTrue("A porta devia estar inicialmente aberta",
                        _portaCobaia.estaAberta());
        _portaCobaia.fechar();
        _portaCobaia.trancar();
        assertFalse("Ao fechar e trancar uma porta aberta, esta ficou aberta",
                        _portaCobaia.estaAberta());
        _portaCobaia.abrir();
        assertFalse("Foi possivel abrir uma porta que foi trancada",
                        _portaCobaia.estaAberta());
        _portaCobaia.destrancar();
        assertFalse("O destrancar de uma porta fez com que esta ficasse aberta",
                        _portaCobaia.estaAberta());
        _portaCobaia.abrir();
        assertTrue("Ao destrancar e abrir uma porta, esta permaneceu fechada",
                        _portaCobaia.estaAberta()); 
    }
}

Note-se que, como fazemos referência à classe TestCase, é necessário importar esta classe definida no "package" junit.framework. Foi definido o método "main" de modo a que para executar os testes baste apenas executar a classe TestePorta.

A nossa classe de testes tem dois testes (os métodos cujos nomes começam por "test"). O primeiro testa o funcionamento das operações de abrir e fechar enquanto que o segundo testa o funcionamento das operações de trancar e destrancar. É importante perceber que ambos os métodos de teste assumem que a porta está inicialmente destrancada e aberta. Isto é possível (sem repetição de código) graças ao método setUp (nome reservado) que é automaticamente executado antes da execução de cada método de teste.

Um "assert" serve para verificar se determinada situação se verifica pois quando tal não acontecer, a falha será relatada. Qualquer dos "asserts" pode e deve receber como primeiro argumento a mensagem que deve ser mostrada quando ocorre a respectiva falha. Neste exemplo usámos o assertFalse, assertTrue e assertEquals. Como seria de esperar, o primeiro verifica se um dado objecto tem o valor true, o segundo verifica se tem o valor false e o terceiro verifica se dois objectos são iguais (através do método equals existente para qualquer objecto). É de notar que os objectos que queremos comparar com assertEquals devem estar na seguinte sequência: Primeiro o valor que estamos à espera de encontrar e em seguida o valor que encontramos. Assim, caso exista uma falha no teste, a mensagem de erro indicará correctamente esta mesma informação.

Testes mais exaustivos poderiam ser definidos. Cabe ao programador elaborar testes que abranjam o maior número de casos tipo para garantir uma melhor fiabilidade do seu código. O que pode parecer trabalho inútil para programas tão simples, pode levar à detecção de erros que, de outro modo, quando programas mais complexos integrassem estas classes, levariam muito mais tempo a depurar.

Se estiver na directoria c:\javacode\porta\ pode compilar o código da seguinte forma:

javac -classpath c:\junit3.8\junit.jar *.java

n.b. Substitua c:\junit3.8\ pela directoria onde instalou o Junit.

n.b. Se o Junit estiver no seu classpath não necessita de utilizar o parâmetro -classpath da JVM.

Após a execução deste comando, dois ficheiros (Porta.class e TestPorta.class) deverão ter sido criados na mesma directoria. Para correr o teste, basta executar a classe junit.textui.TestRunner, dando-lhe como parâmetro a nossa classe de teste TestePorta. Isto pode ser feito da seguinte forma:

java -classpath .;c:\junit3.8\junit.jar junit.textui.TestRunner TestePorta

Se tudo tiver sido bem feito o resultado deverá ser:

..

Time: 0 
OK (2 tests)

O método fail pode também receber uma dada mensagem de erro e serve para forçar que o teste falhe (equivale à instrução "assertTrue(false)").Este método pode ser útil quando queremos indicar que se chegarmos a uma determinada linha no nosso código de testes é porque algo falhou.

Para que possamos testar se um dado método lança ou não uma excepção numa determinada situação basta que essa situação seja simulada e que se tente apanhar a eventual excepção através de um bloco de "try-catch". Se a excepção não devia ocorrer basta que se coloque um "fail" na área de "catch", onde a excepção é tratada. Caso contrário, se a excepção deve ocorrer, basta que se coloque um "fail" na linha imediatamente a seguir à instrução que a lança, ainda dentro da área de "try".

Vejamos um exemplo. O método estático "parseInt" da classe "Integer" permite converter uma "String" num "int" e lançará uma "NumberFormatException" se a String não contiver um número inteiro válido. Eis a assinatura deste método:

public static int parseInt( String s) throws NumberFormatException

A seguinte classe permite testar se o método lança as excepções só e apenas quando deve, da forma que explicámos atrás.

  import junit.framework.TestCase;
  
  public class TesteParseInt extends TestCase {
  
    public static void main(String[] args) {
        junit.textui.TestRunner.run(TesteParseInt.class);
    }
  
    public void testExcepcoes() {
        final String NUMERO = "10";
        final String NAO_NUMERO = "ola";
        try {
            Integer.parseInt(NUMERO);
        } catch (NumberFormatException excepcao) {
            fail("Foi lancada uma excepcao ao fazer o parsing de \""
                + NUMERO + "\": " + excepcao);
        }
        try {
            Integer.parseInt(NAO_NUMERO);
            fail("Nao foi lancada excepcao ao fazer o parsing de \""
                + NAO_NUMERO + "\"");
        } catch (NumberFormatException excepcao) {
        }
    }
  }

O JUnit vem também acompanhado de interfaces gráficas (junit.awtui.TestRunner e junit.swingui.TestRunner). Para mais informação sobre a sua utilização, consulte o ficheiro doc\cookbook\cookbook.htm acessível a a partir da directoria de instalação.

Uma nota final: a maioria dos IDE Java permite configurar directorias de bibliotecas Java para cada aplicação e utilizá-las de forma transparente no processo de criação da mesma, evitando a configuração da variável classpath. É algo que merece ser investigado pois pode facilitar o seu processo de trabalho. No entanto, deverá conhecer os procedimentos acima para os casos em que apenas tiver uma linha de comando num terminal não gráfico à sua disposição.

O arquivo de instalação contêm um conjunto de documentos, tutoriais e exemplos valiosos para a compreensão do framework. Inclui também a descrição completa da API. Após correr o sua primeira unidade de testes, investigue como ligar hierarquicamente várias unidades de testes e como activar um particular teste de um conjunto de unidades.