quarta-feira, 6 de junho de 2012

Mocks com Mockito - Parte 3

Neste post vamos abordar o uso de Spies usando o Mockito.

Ao contrário de Mocks com os Spies é possível chamar os métodos reais da classe.

List<String> listaSpy = new ArrayList<String>();
listaSpy = Mockito.spy(listaSpy);

listaSpy.add("item1");
//Chama o método verdadeiro do objeto e retorna a String = "item1"
listaSpy.get(0); 
Mockito.doReturn("outra coisa").when(listaSpy).get(0);
//Não chama o método verdadeiro e retorna a String = "outra coisa"
listaSpy.get(0); 

Ao contrário dos Mocks um objeto Spy age como se fosse um Semi mock, em outras palavras,  quando não definimos um outro comportamento para o Spy, seu funcionamento é igual ao de um objeto normal. No entanto ao definirmos um outro comportamento (linha 8) ele se comporta como um Mock para o caso que definimos.

Ok e eu com isso?! Vamos ver um uso mais real de um spy.

O uso que eu tive ao usar o Spy foi para Mockar criação/instanciação de objetos.

Por exemplo, na classe Connection temos o seguinte método.

//declaração da classe Connection
public boolean connect() {
  try {
    socket = new Socket(hostAddress, port);
    printStream = new PrintStream(socket.getOutputStream());
    bufferedReader = new BufferedReader(
                       new InputStreamReader(socket.getInputStream()));
    return true;
  } catch (UnknownHostException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }

  return false;
}

public String readMessage() { 
//...
  return bufferedReader.readLine();
//...
}

Como faremos para testar esta classe e o método readLine(), se no cenário atual nós não podemos criar um mock de BufferedReader em nosso teste e passa-la para o objeto Connect, pois o BufferedReader é instanciado dentro da classe Connect!. :/

Spy ao seu resgate! Uma solução é usar Spies para simular isso... então nosso primeiro passo é criar métodos na classe Connect que criem os objetos que precisamos.

Ficando assim o código.

//declaração da classe Connection
public boolean connect() {
  //..
  socket = createSocket();
  printStream = createPrintStream();
  bufferedReader = createInputStream();
  return true;
  //...
}

public String readMessage() { 
  //...
  return bufferedReader.readLine();
  //...
}

protected Socket createSocket() //...
protected PrintStream createPrintStream() //...
protected BufferedReader createInputStream() //...


Veja que os métodos que criam os objetos são protected, isso é necessário pois o Mockito não consegue interceptar métodos que são privados.

O teste da classe ficará assim:

import static org.mockito.Mockito.*;
import static org.junit.Asserts.*;

@Test public void shouldReadMessage() throws UnknownHostException, IOException {
  //cria instancia da classe a ser testada
  Connection connection = new Connection("127.0.0.1", 14000);
  //transforma em um spy
  connection = spy(connection);
  //cria os mocks que serão retornados 
  //pelos metodos de criação de objetos 
  Socket socket = mock(Socket.class);
  BufferedReader bufferedReader = mock(BufferedReader.class);
  PrintStream printStream = mock(PrintStream.class);

  //define o comportamento dos métodos 
  doReturn(socket).when(connection).createSocket();
  doReturn(bufferedReader).when(connection).createInputStream();
  doReturn(printStream).when(connection).createPrintStream();
  when(bufferedReader.readLine()).thenReturn("readed from mock");

  connection.connect();
  assertNotNull(connection.readMessage());
}

Veja que apesar de termos que alterar a nossa classe e extrair as criações de objetos para métodos protected, foi possível criar um teste do método readMessage usando mocks dos objetos do socket.

O uso de Spies da maneira que foi mostrada, pode indicar que seu código esteja com um acoplamento alto e como sempre cabe pensar um pouco se não existe um jeito melhor de programar sua classe para evitar isto.

Isso conclui nossa série de posts sobre o Mockito, pelo menos por enquanto, espero muito que a pequena introdução que escrevi aqui os ajudem a criar seus testes unitários assim como tem me ajudado bastante.

Segue os links que usei de referência
Mocking object creation
Mockito class javadoc
Site do Mockito

Código fonte: https://github.com/diegoy/talks-mockito

Qualquer dúvida, como sempre perguntem, farei o possível para ajudar.

3 comentários:

  1. Este comentário foi removido pelo autor.

    ResponderExcluir
  2. Parabéns Diego, muito bom o post.

    No caso do spy, não seria mais "puro" usar uma classe anônima?

    Não me sinto muito confortável de testar um classe/subject com spy, uma classe com cglib.

    Fiz alguns exemplos de refactory de código legado até uma implementação "ideal". O primeiro passo é com classe anônima.

    https://gist.github.com/3108659

    O que você acha?

    Será que é só preconceito meu com o spy? hehehe

    ResponderExcluir
    Respostas
    1. De verdade eu acho que você tá certo os spies devem ser usados com cuidado.

      Se você ver mesmo na documentação do Mockito sobre spies eles não recomendam muito o uso pois, de verdade usa-los indica que você possui um código altamente acoplado.

      E isso é ruim, mesmo que você consiga usar um spy para testar :) o que ajuda em parte não muda o fato de você ter um alto acoplamento.

      Eu achei boa sua solução :) e é preferível fazer um refactory e arrumar o código que manter do jeito atual.

      E tem até um se você ver no wiki do Mockito, tem a seguinte página que te dá alternativas de como Mockar criação de objetos, http://code.google.com/p/mockito/wiki/MockingObjectCreation.

      Valeu pelo comentário

      Excluir