terça-feira, 23 de março de 2010

Como testar com JUnit

Neste post, vou mostrar como é fácil usar a versão 4.8.1 do JUnit, que pode ser encontrada no site oficial. Além disso, pretendo mostrar como não ferir a Lei de Demeter em uma situação corriqueira, objetos com listas.

Quando estão trabalhando com listas, algumas pessoas tendem em apenas expor o atributo através de um getter. É mais simples.




Mas Fowler, em Refactoring: Improving the Design of Existing Code, diz que:

A method returns a collection.
Make it return a read-only view and provide add/remove methods.
E a Lei de Demeter diz:

The fundamental notion is that a given object should assume as little as possible about the structure or properties of anything else (including its subcomponents).
É impressionante a quantidade de legado que encontro que não respeita esta simples lei. O benefício deste encapsulamento, para quem ainda não tenha ficado claro, é a facilidade de manutenção. Vamos dizer que a implementação inicial da lista seja um ArrayList. Alterar essa implementação para um Set, por exemplo, não afetará em absolutamente nada o cliente da classe que possui a lista. Se a teoria ainda não deixou claro, o exemplo, com certeza, fará o trabalho.


O TFD diz que devemos escrever o teste antes da funcionalidade. Para isso, o desenvolvedor tem que conhecer muito bem a funcionalidade. Deve saber bem como deve ser aquele comportamento.

Vamos ao exemplo clássico de Pedido e Item, que vai permitir a demonstração de acesso a listas, obedecendo a Lei de Demeter, e o TDD sobre as funcionalidades de acesso.

Classe de teste:

package br.com.celsomartins.tdd.tests;

import static junit.framework.Assert.assertEquals;

import org.junit.Test;

public class PedidoTest {
   
    @Test
    public void testAddPedido(){
        Pedido pedido = new Pedido();
        ItemPedido item = new ItemPedido(5, 10.0);
        int qtdPedidos = pedido.addItem(item);
       
        assertEquals(1, qtdPedidos);
    }
   
    @Test
    public void testRemovePedido(){
        Pedido pedido = new Pedido();
       
        ItemPedido item01 = new ItemPedido(5, 10.0);
        ItemPedido item02 = new ItemPedido(7, 250.0);
        pedido.addItem(item01);
        pedido.addItem(item02);
       
        int qtdPedidos = pedido.removeItem(item01);
       
        assertEquals(1, qtdPedidos);
    }
   
    @Test
    public void testGetQtdItens(){
        Pedido pedido = new Pedido();
       
        ItemPedido item01 = new ItemPedido(5, 10.0);
        ItemPedido item02 = new ItemPedido(7, 250.0);
        pedido.addItem(item01);
        pedido.addItem(item02);
       
        int qtdItens = pedido.getQtdItens();
       
        assertEquals(2, qtdItens);
    }
   
    @Test
    public void testGetQtdTotalItens(){
        Pedido pedido = new Pedido();
       
        ItemPedido item01 = new ItemPedido(5, 10.0);
        ItemPedido item02 = new ItemPedido(7, 250.0);
        pedido.addItem(item01);
        pedido.addItem(item02);
       
        int qtdTotalItens = pedido.getQtdTotalItens();
        int esperado = 12;
       
        assertEquals(esperado, qtdTotalItens);
    }
   
    @Test
    public void testGetValorPedido(){
        Pedido pedido = new Pedido();
       
        ItemPedido item01 = new ItemPedido(5, 10.0);
        ItemPedido item02 = new ItemPedido(7, 250.0);
        pedido.addItem(item01);
        pedido.addItem(item02);
       
        double valorPedido = pedido.getValor();
        double esperado = 260.0;
       
        assertEquals(esperado, valorPedido);
    }
}


Neste momento, a classe PedidoTest possui 28 erros de compilação, todos referente a não implementação das classes Pedido e ItemPedido.

Vamos implementa-las.

Primeiro a classe Pedido:

package br.com.celsomartins.tdd.pedido;

import java.util.ArrayList;
import java.util.List;

public class Pedido {
   
    // Neste caso, o encapsulamento é real, pois não estamos apenas
    // expondo o atributo através de getter e setter "burro", mas
    // disponibilizando métodos, onde a própria classe poderá
    // manipular seus atributos. Esta é a coesão e o verdadeiro encapsulamento.
    private List<ItemPedido> listaDeItens = new ArrayList<ItemPedido>();
   
    public Pedido(){
       
    }
   
    public Integer addItem(ItemPedido item){
        return 0;
    }
   
    public Integer removeItem(ItemPedido item){
        return 0;
    }
   
    public Integer getQtdItens(){
        return 0;
    }
   
    public Integer getQtdTotalItens(){
        return 0;
    }
   
    public Double getValor(){
        return 0.0;
    }
}


Classe ItemPedido:

package br.com.celsomartins.tdd.pedido;

public class ItemPedido {
   
    private Integer qtd = 0;
    private Double valor = 0.0;
   
    public ItemPedido(Integer qtd, Double valor) {
        super();
        this.qtd = qtd;
        this.valor = valor;
    }

    public Integer getQtd() {
        return qtd;
    }

    public Double getValor() {
        return valor;
    }
}


Neste momento, eliminamos os problemas de compilação com stub dos métodos que deverão implementar as funcionalidades testadas. Vamos rodar a nosso teste. Neste caso, poderíamos rodar apenas a classe de teste separadamente. Entretanto, como uma suite de testes é, normalmente, necessária, vamos mostrar como é simples criar uma com o JUnit 4.8.1.

A suite:

package br.com.celsomartins.tdd.tests;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(value=Suite.class)
@SuiteClasses(value={
        PedidoTest.class
    }
)
public class TestAll {

}


Simples assim. Para adicionar mais classes de teste na suite, apenas as referencie na anotação @SuiteClasses, separando por vírgula.

Ao rodar nosso teste, recebemos 5 execuções com falha. Esses são os erros funcionais que precisamos corrigir. Vamos implementar o primeiro método da classe pedido:

public Integer getQtdItens(){
       return listaDeItens.size();
}
 


Rodamos a suite novamente e só recebemos 4 erros funcionais. Implementamos um a um e rodamos a suite, para nos certificarmos que tudo continua bem. A classe Pedido, após as implementações, fica dessa forma:

package br.com.celsomartins.tdd.pedido;

import java.util.List;
import java.util.ArrayList;

public class Pedido {
   
    // Neste caso, o encapsulamento é real, pois não estamos apenas
    // expondo o atributo através de getter e setter "burro", mas
    // disponibilizando métodos, onde a própria classe poderá
    // manipular seus atributos. Este é a coesão e o verdadeiro encapsulamento.
    private List<ItemPedido> listaDeItens = new ArrayList<ItemPedido>();
   
    public Pedido(){
       
    }
   
    public Integer addItem(ItemPedido item){
        listaDeItens.add(item);
        return getQtdItens();
    }
   
    public Integer removeItem(ItemPedido item){
        listaDeItens.remove(item);
        return getQtdItens();
    }
   
    public Integer getQtdItens(){
        return listaDeItens.size();
    }
   
    public Integer getQtdTotalItens(){
        int soma = 0;
        for (ItemPedido item: listaDeItens){
            soma += item.getQtd();
        }
        return soma;
    }
   
    public Double getValor(){
        Double soma = 0.0;
        for (ItemPedido item: listaDeItens){
            soma += item.getValor();
        }
        return soma;
    }
}


Pronto, todos os testes rodam e os analistas vão para suas casas felizes.

A facilidade de testes, que na verdade é o que garante a qualidade da sua aplicação, é notória. Testar manualmente estas funcionalidades, demandaria muito tempo. Mesmo com processo de deploy automatizado, ainda teríamos que usar o sistema a cada alteração, para identificar se houve alguma alteração comportamental. Com testes automatizados, só precisamos escrever o teste e depois apertar "um botão", quantas vezes forem necessárias.

O TDD tende a 100% de cobertura, da sua aplicação, por testes automatizados. E é um dos fundamentos das metodologias ágeis existentes hoje em dia, como a XP e o Scrum.


Agora vamos observar o benefício da aplicação da Lei de Demeter, com o encapsulamento adequado. Vamos dizer que o cliente decidiu que os itens de um pedido devem vir sempre ordenados e não pode haver duplicatas. Teríamos que alterar a interface da lista para Set e a implementação para TreeSet. Só precisamos de alteração na classe Pedido, pois somente ela possui o controle sobre a lista de itens. O atributo listaDeItens fica desta forma:

private Set<ItemPedido> listaDeItens = new TreeSet<ItemPedido>();

Neste caso, pouca alteração na classe Pedido, entretanto, se uma outra camada estivesse manipulando a lista interna de Pedido, esta camada também precisaria ser alterada.

Escrever os testes automatizados demanda tempo, mas não tenho dúvidas de que, na verdade, economiza muito mais tempo que testes manuais. Com os testes manuais, alguns testes são esquecidos durante o processo, o que pode levar a problemas no futuro. A garantia de qualidade da aplicação é muito maior com os testes automatizados. E com a tendencia a 100% de cobertura do TDD, aumentamos, claramente, a garantia de qualidade do que estamos desenvolvendo.

http://xprogramming.com/articles/testfirstguidelines/
http://www.objectmentor.com/resources/articles/tfd.pdf

4 comentários:

  1. Nunca usei o JUnit, mas de cara sinto falta de algo para testar as interfaces com o usuário. Existe algum complemento do JUnit que sirva a esse propósito?

    Imagino também que não seja tarefa das mais simples escrever casos de teste para classes que alterem certos estados, como por exemplo, gravar algo no banco de dados (ou, pior ainda, invoque um serviço remoto que grave alguma coisa no banco do outro lado).

    Qual a técnica utilizada nesse caso? Algum controle de transação (para dar rollback depois) ou utilização de mocks? Acho que isso vale um outro artigo, hein?

    ResponderExcluir
  2. @Cotta, em primeiro lugar, obrigado por prestigiar este espaço. É um orgulho ter você por aqui, já que conheço sua capacidade profissional, pois trabalhamos juntos. =)

    Vamos às questões:

    Em primeiro lugar, precisamos deixar toda a lógica de negócio na camada Modelo (Model do MVC). Dessa forma, conseguiremos testes eficientes. Se a view só estiver com a sua responsabilidade, isto é, apresentar os dados e não os estiver manipulando (como vemos frequentemente nos projetos legados que atuamos juntos), acredito que a importancia do teste nesta camada seja reduzida. Nunca usei, mas também nunca senti falta. Mas já li sobre ferramentas para este tipo de teste também.

    Com relação aos testes para acesso a dados, existe o DBUnit: http://www.dbunit.org/

    Todos os artigos que li sobre o assunto, repetem a mesma coisa: Mantenha suas regras de negócio no modelo. Mantenha a complexidade nesta camada. Desta forma, podemos usar Mock Objects para representar "the outside world".

    Quando invocamos serviços assincronos, que acredito que tenha sido a sua questão, já que com serviços sincronos podemos simplesmente testar o retorno, acredito que, para a aplicação, o importante seja a chamada do serviço estar funcionando. Quando eu atuava com o Vitria BW, percebi que o Sistema A, por exemplo, envia uma mensagem ao Sistema B, não se importando nem se o Sistema B recebeu, nos casos assincronos. Então, só precisaríamos testar o envio do Sistema A, por exemplo. O que o Sistema B fez com aquela mensagem, ou até mesmo se recebeu, não é mais problema do Sistema A.

    Abraços

    ResponderExcluir
  3. @Renan
    Obrigado pelo elogio e pela participação.

    ResponderExcluir