Spy no Mockito

JavaBeginner
Pratique Agora

Introdução

Mockito é um framework Java poderoso usado para mocking (criação de mocks) e testes unitários de aplicações Java. Neste laboratório, exploraremos o conceito de spies (espiões) no Mockito. Um Spy é uma ferramenta de teste única que age como um mock parcial - ele rastreia as interações com um objeto, enquanto ainda permite que os métodos reais sejam chamados.

Este comportamento dual faz com que os spies sejam particularmente úteis quando queremos verificar as interações com objetos, mas ainda precisamos que a funcionalidade real seja executada. Ao longo deste laboratório, aprenderemos como criar spies usando diferentes abordagens, entender como stubar (definir o comportamento) de um spy e comparar spies com mocks para destacar suas principais diferenças.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível intermediário com uma taxa de conclusão de 68%. Recebeu uma taxa de avaliações positivas de 97% dos estudantes.

Criando um Projeto Java e Entendendo o Mockito Spy

Neste passo, configuraremos a estrutura do nosso projeto e entenderemos o que é um Spy no Mockito. Spies nos permitem rastrear chamadas de métodos em objetos reais, enquanto ainda usamos sua implementação real.

Criar o Arquivo Java

Primeiro, vamos criar nosso arquivo Java principal para este laboratório. No WebIDE, crie um novo arquivo chamado MockitoSpyDemo.java no diretório ~/project.

Criar um novo arquivo

Adicione o seguinte código para importar as dependências necessárias:

import org.junit.Test;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.MockitoAnnotations;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;

public class MockitoSpyDemo {

    public static void main(String[] args) {
        System.out.println("This is a Mockito Spy demonstration.");
        System.out.println("To run the tests, you need to use JUnit.");
    }
}

Entendendo o Mockito Spy

Um Spy no Mockito é um wrapper (invólucro) em torno de um objeto real que permite:

  1. Rastrear todas as interações com o objeto (como um mock)
  2. Executar os métodos reais do objeto (diferente de um mock)

Pense em um spy como um observador que assiste o que acontece com um objeto, mas não interfere em seu comportamento normal, a menos que seja especificamente programado para fazê-lo.

Vamos adicionar nosso primeiro método de teste para demonstrar o uso básico de um spy. Adicione o seguinte método à sua classe MockitoSpyDemo:

@Test
public void basicSpyExample() {
    // Create a list and spy on it
    List<String> realList = new ArrayList<>();
    List<String> spyList = Mockito.spy(realList);

    // Using the spy to add an item (the real method is called)
    spyList.add("Hello");

    // Verify the interaction happened
    Mockito.verify(spyList).add("Hello");

    // The real method was called, so the list actually has the item
    assertEquals(1, spyList.size());
    assertEquals("Hello", spyList.get(0));

    System.out.println("Basic Spy Example - Spy List Content: " + spyList);
    System.out.println("Basic Spy Example - Spy List Size: " + spyList.size());
}

Vamos compilar e executar nosso código para ver se funciona:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java
java -cp .:lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo
Run the code

Você deve ver a seguinte saída:

This is a Mockito Spy demonstration.
To run the tests, you need to use JUnit.

Nosso método principal é executado corretamente. No entanto, para executar os testes, precisamos usar JUnit. Em vez de configurar um executor JUnit complexo para este laboratório, focaremos em entender a estrutura e os conceitos do código.

No próximo passo, exploraremos diferentes maneiras de criar spies no Mockito.

Diferentes Maneiras de Criar Spies no Mockito

Neste passo, exploraremos diferentes abordagens para criar spies no Mockito. Aprenderemos como criar spies usando o método Mockito.spy() e a annotation @Spy.

Usando o Método Mockito.spy()

A maneira mais direta de criar um spy é usando o método Mockito.spy(). Vamos adicionar um método de teste que demonstra essa abordagem:

@Test
public void testSpyWithMockitoSpyMethod() {
    // Create an ArrayList and a spy of it
    ArrayList<Integer> realList = new ArrayList<>();
    ArrayList<Integer> spyList = Mockito.spy(realList);

    // Adding elements to the spy (real methods are called)
    spyList.add(5);
    spyList.add(10);
    spyList.add(15);

    // Verifying interactions
    Mockito.verify(spyList).add(5);
    Mockito.verify(spyList).add(10);
    Mockito.verify(spyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, spyList.size());

    System.out.println("Spy Method Example - Spy List Content: " + spyList);
    System.out.println("Spy Method Example - Spy List Size: " + spyList.size());
}

Usando a Annotation @Spy

Outra maneira de criar um spy é usando a annotation @Spy. Essa abordagem é particularmente útil quando você deseja inicializar spies automaticamente. Adicione o seguinte código à sua classe MockitoSpyDemo:

// Declare a spy using annotation
@Spy
ArrayList<Integer> annotationSpyList = new ArrayList<>();

@Before
public void initSpies() {
    // Initialize mocks and spies with annotations
    MockitoAnnotations.initMocks(this);
}

@Test
public void testSpyWithAnnotation() {
    // Adding elements to the spy
    annotationSpyList.add(5);
    annotationSpyList.add(10);
    annotationSpyList.add(15);

    // Verifying interactions
    Mockito.verify(annotationSpyList).add(5);
    Mockito.verify(annotationSpyList).add(10);
    Mockito.verify(annotationSpyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, annotationSpyList.size());

    System.out.println("Annotation Spy Example - Spy List Content: " + annotationSpyList);
    System.out.println("Annotation Spy Example - Spy List Size: " + annotationSpyList.size());
}

Entendendo a Diferença Entre as Abordagens

Ambas as abordagens criam um spy, mas têm casos de uso diferentes:

  1. Mockito.spy() é mais flexível e pode ser usado em qualquer lugar em seus métodos de teste.
  2. A annotation @Spy é mais conveniente quando você tem muitos spies e deseja inicializá-los todos de uma vez.

O método MockitoAnnotations.initMocks(this) é responsável por inicializar todos os campos anotados. Em projetos reais, você pode usar @RunWith(MockitoJUnitRunner.class) para ter essa inicialização feita automaticamente.

Vamos compilar nosso código atualizado:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

O código deve compilar sem erros. Nossos métodos de teste estão agora prontos, mas, como mencionado anteriormente, precisaríamos de um executor JUnit para executá-los automaticamente.

No próximo passo, aprenderemos como stubar (definir o comportamento) spies para substituir seu comportamento padrão.

Stubbing Spies e Tratamento de Exceções no Mockito

Neste passo, aprenderemos como stubar (definir o comportamento) um spy para substituir seu comportamento padrão e entender como lidar com exceções que podem ocorrer ao usar o Mockito.

Stubando um Spy

Spies usam métodos de objetos reais por padrão, mas às vezes queremos mudar esse comportamento para fins de teste. Isso é chamado de "stubbing" (definir o comportamento) do spy. Vamos adicionar um método de teste que demonstra como stubar um spy:

@Test
public void testStubbingSpy() {
    // Create a spy of ArrayList
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add an element
    spyList.add(5);

    // Verify the element was added
    Mockito.verify(spyList).add(5);

    // By default, contains() should return true for element 5
    assertTrue(spyList.contains(5));
    System.out.println("Stubbing Example - Default behavior: spyList.contains(5) = " + spyList.contains(5));

    // Stub the contains() method to always return false for the value 5
    Mockito.doReturn(false).when(spyList).contains(5);

    // Now contains() should return false, even though the element is in the list
    assertFalse(spyList.contains(5));
    System.out.println("Stubbing Example - After stubbing: spyList.contains(5) = " + spyList.contains(5));

    // The element is still in the list (stubbing didn't change the list content)
    assertEquals(1, spyList.size());
    assertEquals(Integer.valueOf(5), spyList.get(0));
    System.out.println("Stubbing Example - Spy List Content: " + spyList);
}

Nota Importante Sobre Stubbing

Ao stubar métodos spy, é recomendado usar doReturn(), doThrow(), doAnswer(), etc., em vez de when(). Isso ocorre porque, com spies, a sintaxe when().thenReturn() pode chamar o método real durante o stubbing, o que pode causar efeitos colaterais inesperados.

Por exemplo:

// This might call the real get() method, causing issues if index 10 doesn't exist
Mockito.when(spyList.get(10)).thenReturn(99); // May throw IndexOutOfBoundsException

// This is the correct way to stub a spy
Mockito.doReturn(99).when(spyList).get(10); // Safe, doesn't call the real method

Lidando com NotAMockException

Se você tentar usar os métodos de verificação do Mockito em um objeto regular (não um mock ou spy), você obterá uma NotAMockException. Vamos adicionar um método de teste para demonstrar essa exceção:

@Test
public void testNotAMockException() {
    try {
        // Create a regular ArrayList (not a mock or spy)
        ArrayList<Integer> regularList = new ArrayList<>();
        regularList.add(5);

        // Try to verify an interaction on a regular object
        Mockito.verify(regularList).add(5);

        fail("Expected NotAMockException was not thrown");
    } catch (NotAMockException e) {
        // Expected exception
        System.out.println("NotAMockException Example - Caught expected exception: " + e.getMessage());
        assertTrue(e.getMessage().contains("Argument passed to verify() is not a mock"));
    }
}

Vamos compilar nosso código atualizado:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

O código deve compilar sem erros. Neste passo, aprendemos como stubar métodos spy para substituir seu comportamento padrão e como lidar com a NotAMockException que ocorre ao usar métodos de verificação em objetos regulares.

No próximo passo, compararemos mocks e spies para entender suas principais diferenças.

Mock vs Spy: Entendendo as Diferenças no Mockito

Neste passo, compararemos diretamente mocks e spies para entender suas principais diferenças. Essa comparação o ajudará a escolher a ferramenta certa para suas necessidades de teste.

Criando um Teste de Comparação

Vamos adicionar um método de teste que demonstra as diferenças entre mocks e spies:

@Test
public void testMockVsSpyDifference() {
    // Create a mock and a spy of ArrayList
    ArrayList<Integer> mockList = Mockito.mock(ArrayList.class);
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add elements to both
    mockList.add(1);
    spyList.add(1);

    // Verify interactions (both will pass)
    Mockito.verify(mockList).add(1);
    Mockito.verify(spyList).add(1);

    // Check the size of both lists
    System.out.println("Mock vs Spy - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy - Spy List Size: " + spyList.size());

    // Mock returns default values (0 for size) unless stubbed
    assertEquals(0, mockList.size());

    // Spy uses real method implementation, so size is 1
    assertEquals(1, spyList.size());

    // Stub both to return size 100
    Mockito.when(mockList.size()).thenReturn(100);
    Mockito.when(spyList.size()).thenReturn(100);

    // Both should now return 100 for size
    assertEquals(100, mockList.size());
    assertEquals(100, spyList.size());

    System.out.println("Mock vs Spy (After Stubbing) - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy (After Stubbing) - Spy List Size: " + spyList.size());
}

Entendendo as Principais Diferenças

Vamos resumir as principais diferenças entre mocks e spies no Mockito:

  1. Comportamento do Método:

    • Mock: Todos os métodos retornam valores padrão (null, 0, false) a menos que sejam stubados (definidos o comportamento)
    • Spy: Os métodos chamam a implementação real, a menos que sejam stubados
  2. Estado do Objeto:

    • Mock: As operações não alteram o estado (por exemplo, adicionar a uma lista mock não adiciona nada)
    • Spy: As operações alteram o estado do objeto, assim como um objeto real
  3. Criação:

    • Mock: Criado a partir de uma classe, não de uma instância (Mockito.mock(ArrayList.class))
    • Spy: Geralmente criado a partir de uma instância (Mockito.spy(new ArrayList<>()))
  4. Casos de Uso:

    • Mock: Quando você deseja controlar completamente o comportamento e não precisa de métodos reais
    • Spy: Quando você deseja usar métodos reais, mas precisa substituir apenas alguns comportamentos

Exemplo Prático: Quando Usar um Spy

Vamos adicionar mais um teste que mostra um caso de uso comum para spies - stubar (definir o comportamento) parcialmente um objeto complexo:

@Test
public void testWhenToUseSpy() {
    // Create a complex data object that we want to partially mock
    StringBuilder builder = new StringBuilder();
    StringBuilder spyBuilder = Mockito.spy(builder);

    // Use the real append method
    spyBuilder.append("Hello");
    spyBuilder.append(" World");

    // Verify the real methods were called
    Mockito.verify(spyBuilder).append("Hello");
    Mockito.verify(spyBuilder).append(" World");

    // The real methods modified the state
    assertEquals("Hello World", spyBuilder.toString());
    System.out.println("When to Use Spy - Content: " + spyBuilder.toString());

    // Stub the toString() method to return something else
    Mockito.when(spyBuilder.toString()).thenReturn("Stubbed String");

    // Now toString() returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
    System.out.println("When to Use Spy - After Stubbing toString(): " + spyBuilder.toString());

    // But other methods still work normally
    spyBuilder.append("!");
    Mockito.verify(spyBuilder).append("!");

    // toString() still returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
}

Vamos compilar nosso código atualizado:

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

O código deve compilar sem erros. Neste passo, aprendemos as principais diferenças entre mocks e spies e quando usar cada um. Mocks substituem completamente o comportamento do objeto original, enquanto spies mantêm o comportamento original, mas permitem que você substitua métodos específicos quando necessário.

Resumo

Neste laboratório, exploramos o conceito de spies no Mockito, um recurso poderoso para testes unitários em Java. Aqui estão os pontos-chave que abordamos:

  1. Criando Spies: Aprendemos como criar spies usando o método Mockito.spy() e a annotation @Spy. O primeiro é mais flexível, enquanto o último é mais conveniente quando você tem muitos spies.

  2. Comportamento do Spy: Spies rastreiam chamadas de métodos como mocks, mas executam os métodos reais por padrão. Essa natureza dupla os torna particularmente úteis para testar objetos reais, mantendo a capacidade de verificar interações.

  3. Stubando Spies: Descobrimos como substituir o comportamento padrão dos métodos spy usando doReturn(), doThrow() e outros métodos de stubbing. Isso nos permite controlar comportamentos específicos, mantendo outros intactos.

  4. Tratamento de Exceções: Exploramos a NotAMockException que ocorre ao tentar usar métodos de verificação do Mockito em objetos regulares e como lidar com ela corretamente.

  5. Mock vs Spy: Comparamos mocks e spies para entender suas principais diferenças. Mocks substituem completamente o comportamento original com respostas padrão ou stubadas, enquanto spies mantêm o comportamento original, mas permitem substituir métodos específicos.

Spies são uma ferramenta poderosa no arsenal do Mockito, oferecendo um equilíbrio entre o controle completo dos mocks e o realismo dos objetos reais. Eles são particularmente úteis quando:

  • Você deseja testar um objeto real, mas precisa verificar certas interações
  • Você precisa substituir apenas métodos específicos, mantendo o restante do comportamento do objeto intacto
  • Você está trabalhando com objetos complexos onde mockar todos os métodos seria impraticável

Ao entender quando e como usar spies, você pode escrever testes unitários mais eficazes e sustentáveis para suas aplicações Java.