Mockito: como isolar seus testes de unidade no Java

Introdução

Uma das características fundamentais de um bom teste unitário é ser isolado. Para que ele execute rápido, fornecendo feedback ao desenvolvedor, ele deve ser isolado. Quando digo isolado é que ele deve ser isolado de dependências externas: acesso à rede, ao sistema de arquivos, a banco de dados etc. É aí que os mocks podem nos ajudar e que o Mockito entra em ação.

Imagine que você precisa testar um código que faz acesso à camada de persistência por meio de um DAO/Repository. Para esse código funcionar, em produção, é necessário que algum mecanismo de persistência (um banco de dados relacional, por exemplo) esteja disponível. Para o código de testes de unidade isso é impraticável: ele vai ficar lento, mais complexo, vai perder o isolamento.

A solução para o caso anterior é simular o acesso à camada de persistência. Daí o uso de mocks. De maneira geral, para facilitar o entendimento, mocks são objetos criados para simular, de forma controlada, determinados comportamentos de objetos reais. Por forma controlada entenda: como bem quisermos ou desejarmos.

O Mockito, como veremos a seguir, nos auxilia na tarefa de criação de mocks. Vamos, então, conhecer um pouco mais dele.

Mockito e configuração básica

O Mockito é um framework de mocking para uso em testes de unidade na linguagem Java. Ele possui uma API clara e simples. A curva de aprendizado é suave e ele possui uma documentação bastante útil, recheada de exemplos.

Entre as principais características do Mockito estão:

  • setup para a criação de um mockdiferentemente de outros frameworks, é bastante simples;
  • Possibilidade de criar mocks tanto de classes concretas como de interfaces;
  • Criação de mocks por meio de anotações: @Mock;
  • Facilidade na verificação de erros: a verificação de testes que falham é simples, bastando olhar o stack trace disponibilizado;
  • Permite verificar a quantidade de vezes que um método foi chamado;
  • Permite a verificação por meio de matchers de argumentos (anyObject(), anyString() …);
  • Permite a verificação da ordem das chamadas de um método.

Existem, também, algumas restrições para utilizar o Mockito.

O Mockito é ou não é um framework de mocks?
Essa é uma discussão que ocorre com frequência na comunidade. Tecnicamente, o Mockito é um framework do tipo Test and Spy. Frameworks Test and Spy permitem a verificação de comportamento e fazer stub de métodos. Já os mocks, de forma mais técnica, permitem apenas a verificação de comportamento.
Para saber um pouco mais sobre o tema, recomendo a leitura deste excelente artigo do Martin Fowler: Mocks Aren’t Stubs

Configuração via Maven

Para criar testes que fazem uso do Mockito, é necessário adicionar a biblioteca ao classpath de seu projeto. Utilizando o Maven, basta adicioná-lo ao pom.xml do seu projeto, seguindo o exemplo abaixo (no momento da escrita desse post, a versão do Mockito é a 2.2.1).

<dependency>
	<groupId>org.mockito</groupId>
	<artifactId>mockito-core</artifactId>
	<version>2.2.1</version>
	<scope>test</scope>
</dependency>

Para os exemplos utilizados neste post, criei um projeto no GitHub que pode ser acessado aqui.

Como criar um mock com o Mockito

Basicamente há duas formas para se criar um mock com o Mockito: a primeira é por meio de um método estático; a segunda é utilizando uma anotação.

Criando o mock por meio do método estático Mockito.mock

A classe principal do Mockito tem o mesmo nome do framework: Mockito. Ela fornece diversos métodos estáticos. Um deles é o mock(), que recebe como argumento o tipo da classe ou interface que se deseja criar o mock e tem como retorno um objeto mock. O exemplo abaixo mostra como esse método pode ser utilizado.

UsuariosRepository repository = Mockito.mock(UsuariosRepository.class);

Criando o mock por meio da anotação @Mockito

A outra forma de se criar um mock com o Mockito é utilizando a anotação @Mockito. O exemplo abaixo mostra o código com resultado equivalente ao da seção anterior, só que utilizando a anotação.

 @Mock
 private UsuariosRepository repository;

No entanto, para o código anterior funcionar, é necessário utilizar o MockitoJUnitRunner para executar o teste. O código completo para o exemplo anterior funcionar (assumindo o uso do JUnit 4) é apresentado abaixo.

 @RunWith(MockitoJUnitRunner.class)
 public class UsuariosServiceTest {

   @Mock
   private UsuariosRepository repository;

 }

A anotação @RunWith do JUnit, na primeira linha, invoca a classe MockitoJUnitRunner para que ela execute os testes presentes na classe UsuarioServiceTest. Esse runner, dentre outras coisas, inicializa o objeto anotado com @Mock antes da execução de cada método de teste.

Vamos, agora, mostrar alguns casos comuns de uso para aprender outras funcionalidades oferecidas pelo Mockito.

Uso #1: Fazendo o stub do retorno de um método

Um dos usos mais comuns de um framework de mocks é simular o retorno de chamada de um método. E por que fazer isso? A resposta principal é isolamento de dependências. Você pode querer por exemplo, simular o comportamento do seu método em variados tipos de retorno. Por exemplo, veja o código abaixo.

public class TarefaResponsabilizador {

 private TarefasService tarefasService;

 public TarefaResponsabilizador(TarefasService tarefasService) {
    this.tarefasService = tarefasService;
 }

 public void atribuirResponsabilidadeTarefas(Sprint sprint, Usuario usuarioResponsavel) {
    List<Tarefa> tarefas = tarefasService.todas(sprint);

    if (CollectionUtils.isNotEmpty(tarefas)) {
       tarefas.forEach(tarefa -> tarefa.defineResponsavel(usuarioResponsavel));
       tarefasService.salvar(tarefas);
    }
 }

}

Esse é um código que atribui um responsável a todas as tarefas existentes em uma Sprint. Veja que é realizada uma chamada para recuperar todas as tarefas de uma Sprint e depois e feita a atribuição do responsável.

O ponto chave deste código, portanto, é a chamada que recupera as tarefas. Ao escrever um teste para esse código precisamos que a chamada retorne uma lista de tarefas. Vamos utilizar o Mockito para simular o retorno desse método.

O código de teste

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.hasProperty;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;

import java.util.Arrays;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class TarefaResponsabilizadorTest {

  @Mock
  private TarefasService tarefasService;

  @Test
  public void atribuiResponsabilidadeTodasTarefasExistentes() {
    Sprint sprint = new Sprint();
    Usuario usuarioResponsavel = new Usuario();
    List<Tarefa> listaTarefas = Arrays.asList(new Tarefa[] {new Tarefa(), new Tarefa(), new Tarefa()});

    when(tarefasService.todas(sprint)).thenReturn(listaTarefas);

    TarefaResponsabilizador responsabilizador = new TarefaResponsabilizador(tarefasService);
    responsabilizador.atribuirResponsabilidadeTarefas(sprint, usuarioResponsavel);

    assertThat(listaTarefas, everyItem(hasProperty("responsavel", equalTo(usuarioResponsavel))));
  }

}

O código anterior é um caso de teste para o método apresentado anteriormente. Esse caso é bem simples: dada uma lista de tarefas ele verifica se todas as tarefas tiveram o responsável atribuído. A linha em destaque (replicada abaixo) mostra o uso do Mockito nesse teste para simular o retorno da lista de tarefas.

    when(tarefasService.todas(sprint)).thenReturn(listaTarefas);

O que fizemos foi “ensinar” ao Mockito quando (when()) o método todas() do nosso mock tarefasService for chamado, que seja retornada (thenReturn()) a nossa lista de tarefas criada previamente. Com isso, ao executar o código em teste, em vez de o método todas() original ser chamado ele chamará o método em nosso mock.

Desta forma, por meio dessa funcionalidade (Mockito.when().thenReturn()) conseguimos simular diversos comportamentos e, com isso, exercitar diversos caminhos do nosso código.

Dica
Boa parte dos métodos do Mockito são métodos estáticos. Aconselho fortemente o uso de imports estáticos quando trabalhar com o Mockito, pois, o código de teste fica mais curto e mais legível.

 

Uso #2: Assegurando que um método foi chamado

Na listagem do tópico anterior, no método atribuirResponsabilidadeTarefas da classe TarefaResponsabilizador, observamos que após a atribuição do responsável por cada uma das tarefas, é chamado o método salvar() de TarefasService. Replico essa parte do código a seguir.

   tarefas.forEach(tarefa -> tarefa.defineResponsavel(usuarioResponsavel));
   tarefasService.salvar(tarefas);

Ou seja é fundamental que, após a atribuição do responsável pela tarefa, essa mudança seja salva. Portanto, precisamos garantir que essa chamada ao método salvar sempre seja realizada. O Mockito pode nos ajudar nisso. Vamos ao código.

@RunWith(MockitoJUnitRunner.class)
public class TarefaResponsabilizadorTest {

  @Mock
  private TarefasService tarefasService;

  @Test
  public void atribuiResponsabilidadeTodasTarefasExistentes() {
    Sprint sprint = new Sprint();
    Usuario usuarioResponsavel = new Usuario();
    List<Tarefa> listaTarefas = Arrays.asList(new Tarefa[] {new Tarefa(), new Tarefa(), new Tarefa()});
 
    when(tarefasService.todas(sprint)).thenReturn(listaTarefas);
 
    TarefaResponsabilizador responsabilizador = new TarefaResponsabilizador(tarefasService);
    responsabilizador.atribuirResponsabilidadeTarefas(sprint, usuarioResponsavel);
 
    assertThat(listaTarefas, everyItem(hasProperty("responsavel", equalTo(usuarioResponsavel))));
    verify(tarefasService, times(1)).salvar(listaTarefas);
  }
}

Este código de teste é o mesmo anterior, acrescentado apenas da última linha em destaque. Para entender melhor:

  • Utilizamos o método Mockito.verify(). Esse método nos permite verificar a quantidade de vezes que um determinado comportamento aconteceu.
  • O segundo argumento de verify() que utilizamos foi o Mockito.times(), que nos permite checar a quantidade exata de vezes que um método foi chamada. Neste caso, esperamos que o método salvar() de tarefasService seja chamado exatamente 1 vez.

Ao rodar esse teste todas as verificações passam. Se por acaso retirarmos a chamada ao método salvar() no código do TarefaResponsabilizador, o teste falha. Recebemos uma mensagem como a mostrada na figura a seguir.

Teste falhando após a retirada da chamada ao método salvar() de tarefasService.
Teste falhando após a retirada da chamada ao método salvar() de tarefasService.

Veja que o erro é bem descritivo: o Mockito nos avisa que um método deveria ser chamado mas não foi (wanted but not invoked: tarefasService.salvar()).

Uso #3: Simulando a ocorrência de uma exceção

Um outro uso útil do Mockito é simular a ocorrência de uma exceção. Neste caso, queremos ver como o método sob teste se comporta após o lançamento da exceção. Vamos a mais um exemplo.

O trecho de código abaixo é para o envio de uma mensagem de e-mail para vários destinatários.

public class ControladorEnvioEmail {
 
 static final String E_MAIL_ENVIADO_COM_SUCESSO = "E-mail enviado com sucesso!";
 static final String NAO_FOI_POSSIVEL_ENVIAR_TODOS = "Os seguintes e-mails não receberam a mensagem: %s";
 private EmailService emailService;
 
 public StatusOperacao envia(Mensagem mensagem, String remetente, String... destinatarios) {
   List<String> emailsInvalidos = new ArrayList<>();
   for(String destinatario : destinatarios) {
    try {
     emailService.envia(mensagem, remetente, destinatario);
    } catch (EmailInvalidoException e) {
     emailsInvalidos.add(destinatario);
    }
   }
   StatusOperacao status = new StatusOperacao(E_MAIL_ENVIADO_COM_SUCESSO, true);
   if(CollectionUtils.isNotEmpty(emailsInvalidos)) {
     status = new StatusOperacao(String.format(NAO_FOI_POSSIVEL_ENVIAR_TODOS, StringUtils.join(emailsInvalidos, ",")), false);
   }
   return status;
 }

}

Explicando. Percorre-se a lista de destinatários de e-mail e para cada um é enviado um e-mail. Se o e-mail do destinatário for inválido é disparada uma exceção e este e-mail é adicionado a uma lista de e-mails inválidos para que posteriormente seja informado que ele não recebeu a mensagem. O tratamento de exceção (try... catch) serve para garantir que toda a lista de destinatários seja percorrida, impedindo que a execução do código pare no primeiro e-mail inválido encontrado.

O nosso caso de teste

A seguir apresento dois casos de teste para o código anterior de envio de e-mail. O primeiro é o caso de sucesso. Já no segundo simulo o lançamento de uma exceção.

@RunWith(MockitoJUnitRunner.class)
public class ControladorEnvioEmailTest {
 
 @Mock
 private EmailService emailService;
 
 @InjectMocks
 private ControladorEnvioEmail controlador;
 
 @Test
 public void todosDestinatariosRecebemEmail() {
   Mensagem mensagem = new Mensagem();
   String remetente = "bob@codeatest.com";
   String[] destinatarios = new String[] {"john@provedor.com", "liz@provedor.com", "mary@provedor.com"};
   StatusOperacao status = controlador.envia(mensagem, remetente, destinatarios);
   assertTrue(status.isSucesso());
   assertEquals(ControladorEnvioEmail.E_MAIL_ENVIADO_COM_SUCESSO, status.getMensagem());
   verify(emailService, times(3)).envia(Mockito.any(Mensagem.class), Mockito.anyString(), Mockito.anyString());
 }
 
 @Test
 public void destinatarioComEmailInvalidoNaoRecebeEmail() {
   Mensagem mensagem = new Mensagem();
   String remetente = "bob@codeatest.com";
   String[] destinatarios = new String[] {"john@provedor.com", "liz@provedor", "mary@provedor.com"};
 
   doThrow(EmailInvalidoException.class).when(emailService).envia(mensagem, remetente, "liz@provedor");
 
   StatusOperacao status = controlador.envia(mensagem, remetente, destinatarios);
   assertFalse(status.isSucesso());
   assertEquals(String.format(ControladorEnvioEmail.NAO_FOI_POSSIVEL_ENVIAR_TODOS, "liz@provedor"), status.getMensagem());
   verify(emailService, times(3)).envia(Mockito.any(Mensagem.class), Mockito.anyString(), Mockito.anyString());
 }

}

Os 2 casos de testes são bem parecidos. A única diferença está no segundo caso de teste na linha em destaque. Nessa linha utilizamos o método Mockito.doThrow().

O que fizemos foi para que quando o destinatário do e-mail for “liz@provedor” seja lançada uma exceção EmailInvalidoException. Com isso o status da operação passa a ser false e a mensagem de retorno também muda em relação ao primeiro caso de teste. Simples, não é?

Conclusão

O Mockito é um framework bastante completo para o uso de mocks em testes de unidade. É um dos – se não for o mais – frameworks mais utilizados em Java.

Neste post apresentei 3 usos do Mockito que julgo, segundo a minha experiência com testes de unidade em Java, serem os de maior utilidade em casos de teste. Foi uma breve introdução que permite que você, caso ainda não tenha dado, dê seus pontapés iniciais com o framework.

Todo o código deste post encontra-se em meu GitHub e pode ser acessado aqui.

Qualquer crítica ou sugestão – inclusive de novos exemplos que gostaria de ver nesse post, deixe abaixo seu comentário.