Técnica mock em testes unitários

Uma das áreas mais subestimadas do desenvolvimento de software é, sem dúvida, a área de testes. Desde os primórdios do desenvolvimento de sistemas até os dias de hoje, testes são considerados dispendiosos e algo a ser feito depois (o que frequentemente é postergado até o ponto em que acabam não sendo feitos).

Desenvolvedores tendem a não usar testes, por acharem que muito tempo é gasto e que o custo benefício não compensa. Mas, conforme cada vez mais evoluem as técnicas e teorias por trás do desenvolvimento de software, mais e mais estudos confirmam que sim, vale a pena perder o tempo dispensado aos testes e vão além, sugerem que o desenvolvimento deve ser invertido, primeiro criar os testes e depois implementar o necessário para fazer o teste passar (esse método de desenvolvimento é conhecido como TDD = Test Driven Design).

Realmente o TDD é algo interessante e dá pano pra manga pra muito texto, que vou deixar para o futuro.

O que realmente queria falar neste post, é sobre a técnica de criar objetos “falsos” (objetos Mock) com comportamento igual aos objetos reais do seu sistema e que sejam dependências para seus testes, além de mostrar sua utilidade e aplicabilidade em testes funcionais, introduzindo um framework que auxilia na criação destes objetos Mock.

O Problema

Bom, partindo da idéia de que testes (particularmente testes unitários para este post) são sim muito importantes e compensam em benefício o custo de escrevê-los, chega a hora de botar a mão na massa e realmente mudar a cultura de desenvolvimento para incluir a etapa de testes (seja testando depois da implementação, ou desenvolvendo baseado em testes).

Mas, algumas questões surgem na hora de realmente desenvolver o teste. Uma delas é como fazer quando o método sendo testado possui uma chamada a um método de outra classe em uma camada inferior, ou seja, possui uma dependência de outra classe? Por exemplo, ao testar uma classe de negócios, muitas vezes um método desta classe irá chamar um método de alguma classe que manipula informações do bando de dados (classes DAO).

O problema aí é, se você está testando a classe de negócio, seria certo chamar os métodos reais da classe DAO a qual a classe de negócio depende? Não seria um acoplamento muito forte para algo que se supunha testar só o negócio? Você teria que se preocupar com os retornos que esta classe DAO pudesse retornar, etc, quando o foco é testar a classe de negócio.

E mais, como fazer para tratar diferentes comportamentos da classe de negócio, conforme os dados retornados pelo DAO mudassem (não retornar nada, retornar uma lista de dados, etc)?

Antes de falarmos sobre a solução que são os objetos Mock, vamos dar um exemplo para clarificar o problema.

Imagine que você está desenvolvendo um Super Mario em Java, e em determinado momento precisa testar um método de uma classe de negócio que obtém uma lista de jogadores do hall da fama do jogo (jogadores que completaram o jogo) e retorna o número de jogadores que completaram o jogo em menos de 1 hora (ok, funcionalidade estranha, mas foi a primeira que me surgiu na cabeça, e serve para o objetivo do exemplo, então vamos aceitá-la).

O problema é que essa classe de negócio acessa uma classe DAO que por sua vez se conecta ao banco para buscar as informações.

Como se supõe que a classe DAO terá seus testes unitários específicos, seria ruim ter que testar a classe de negócio que, involuntariamente, chama o DAO, mesmo esta já tendo sido testado, e mesmo você não querendo ter de se preocupar com esta dependência, quando seu objetivo é só testar a funcionalidade da regra de negócio!

E mais, como dito anteriormente, você precisaria testar vários cenários para este método. Por exemplo, você precisa testar o comportamento do método quando não existe nenhum jogador ainda no hall da fama, quando existe um, quando existem vários, quando existem muitíssimos, etc. Mas se você apenas chama o método normalmente e este usa o objeto DAO real, somente um cenário será retornado (o cenário real da aplicação) e pior, você não poderá saber que cenário é de anti-mão para poder testá-lo.

Para isso, você pode utilizar os objetos Mock, e implementar o comportamento do objeto da forma que desejar. No nosso exemplo, somente seria necessário que as classes as quais desejamos criar Mocks tenham interfaces que definam seu comportamento.

A idéia é, a partir da interface da classe DAO que é usada pelo nosso objeto de negócio, podemos criar um objeto Mock com o comportamento que quisermos, então, no método de teste do cenário quando nenhum jogador está no hall da fama, somente precisaríamos criar um objeto DAO Mock com a interface que temos e dizer ao objeto o que queremos que ele faça na chamada do método List obterJogadoresHallDaFama(), neste caso retornar uma lista vazia.

Em seguida, precisaríamos poder setar este objeto DAO Mock como o objeto DAO a ser usado pela classe de negócio que estamos testando ao invés da classe DAO “real”, isto pode ser feito implementando um método setterDAO na classe de negócio. Assim poderíamos instanciar a classe DAO “real” (ou injetá-la automaticamente) quando estivessemos usando no contexto da aplicação ou instanciar o objeto Mock durante um contexto de teste.

Tendo toda esta base de conhecimento, surge a dúvida: mas teríamos que ter uma implementação da interface para cada comportamento a ser testado? Precisaríamos criar uma classe que implementasse o método da interface retornando null, outra classe implementando a mesma interface, mas que retornasse uma lista de teste, fora a classe real da aplicação?

A resposta é, você poderia, mas para evitar tanto overhead, existem frameworks próprios para criar estes objetos Mock, entre eles o Mockito, que é simples e fácil de usar e que é usado no exemplo de código abaixo (em Java).

Exemplo

sistema.entidade.Jogador:

public class Jogador {
	private float tempo;

	public float getTempo() {
		return tempo;
	}

	public void setTempo(float tempo) {
		this.tempo = tempo;
	}
}

sistema.dao.JogadorDAO:

public interface JogadorDAO {
    public List<Jogador> obterJogadoresHallFama();
}

sistema.dao.JogadorDAOImpl:

public class JogadorDAOImpl {
    public List<Jogador> obterJogadoresHallFama() {
        // obtem jogadores do hall da fama do BD
    }
}

sistema.bo.JogadorBO:

public interface JogadorBO {
    public void setterJogadorDAO(JogadorDAO dao);
    public int obterNumeroJogadoresHallFamaRapidos();
}

sistema.dao.JogadorBOImpl:

class JogadorBOImpl {
    private JogadorDAO jogadorDAO;

    // setta o objeto dao a ser usado
    public void setterJogadorDAO(JogadorDAO dao) {
        this.jogadorDAO = dao;
    }

    // metodo do negocio que retorna o numero de jogadores 
    // mais rapidos a completar o jogo
    public int obterNumeroJogadoresHallFamaRapidos() {
        List<Jogador> lista = this.jogadorDAO.obterJogadoresHallFama();

        int n = 0;

        for (Jogador j : lista) {

            // compara tempo em horas
            if (j.getTempo() <= 1) n++;
        }   

        return n;
    }
}

sistema.teste.bo.JogadorBOTest:

public class JogadorBOTest {

    // cenario 1: testa quando o dao retorna lista vazia
    @Test
    public void testaObterNumeroJogadoresHallFamaRapidosVazio() {
        JogadorBO jogadorBO = new JogadorBOImpl();

        // cria objeto DAO Mock com Mockito
        JogadorDAO jogadorDAOMock = Mock(JogadorDAO.class);
        when(jogadorDAOMock.obterNumeroJogadores()).thenReturn(null);

        // diz a classe a ser testada que deve usar o DAO Mock que criamos
        jogadorBO.setterJogadorDAO(jogadorDAOMock);

        // faz o teste
        int n = jogadorBO.obterNumeroJogadoresHallFamaRapidos();

        // no cenario em que nao ha jogadores no hall da fama
        // (comportamento que definimos para o objeto Mock deste
        // teste), deve retornar 0
        assertEquals(n, 0);
    }

    // cenario 2: testa quando o dao retorna uma lista com um jogador rapido
    @Test
    public void testaObterNumeroJogadoresHallFamaRapidosVazio() {
        JogadorBO jogadorBO = new JogadorBOImpl();

        // cria jogador Mock
        Jogador jogadorMock = new Jogador();
        jogadorMock.setTempo(0.5);

        // cria lista Mock
        List<Jogador> listaMock = new ArrayList<Jogador>();
        listaMock.add(jogador);

        // cria objeto DAO Mock com Mockito que retorna a lista criada
        JogadorDAO jogadorDAOMock = Mock(JogadorDAO.class);
        when(jogadorDAOMock.obterNumeroJogadores()).thenReturn(listaMock);

        // diz a classe a ser testada que deve usar o DAO Mock que criamos
        jogadorBO.setterJogadorDAO(jogadorDAOMock);

        // faz o teste
        int n = jogadorBO.obterNumeroJogadoresHallFamaRapidos();

        // deve retornar 1, segundo nossa lista retornada
        assertEquals(n, 1);
    }

    /** continua com os testes **/
}

Conclusão

O importante a aprender é que os objetos Mock servem para implementar diferentes cenários nas classes a serem testadas e fazem com que você não tenha de se preocupar com as dependências das classes sendo testadas.

Mas, ainda assim existem vários desafios, como por exemplo, como testar as classes DAO, cujas dependências são os próprios dados do banco de dados? Como fazer para simular os diferentes cenários retornados pelo banco de dados para um dado teste? De forma análoga ao apresentado anteriormente, existem frameworks que criam uma simulação, um Mock dos dados e da conexão do banco de dados. Um dos mais usados é o DBUnit.

Outro desafio é: como testar a parte da interface, testes de aceitação, etc? Como fazer testes de perfomance?

Mas isso tudo fica pra próxima.

Referências

Livro “Use a Cabeça – Desenvolvimento de Software” – Dan Pilone & Russ Miles
Site “Framework Mockito

 

5 comments on “Técnica mock em testes unitários

  1. camilo lopes on said:

    pelo artigo, gostei da introducao e explicacao, porem, sentir de colocar o codigo da classe bean Jogador, alias nao ficou bem claro essa parte, como está a classe jogador.

  2. nessa_uepa on said:

    Olá,

    Obrigada pelo feedback.
    Havia retirado a classe Jogador por ser um POJO simples, mas acho que ficou um pouco nebuloso sem ela.
    Editei o artigo e acrescentei a classe Jogador.
    😉

  3. Cassio Lang on said:

    Parabens, muito bom!

  4. Diego Garcia on said:

    Muito bom o artigo, já me clareou varias ideias.

  5. Leonardo Gonçalves on said:

    Parabéns pelo artigo. Ideias concisas explicadas de maneira clara.
    Ainda não tenho certeza sobre quando vou usar os testes unitários em meu projeto, muito menos mocks, mas este artigo já ampliou meu campo de observações com vistas a novas ideias.
    Grande abraço!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>