Espía (Spy) en Mockito

JavaJavaBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Mockito es un potente framework de Java utilizado para simular (mocking) y realizar pruebas unitarias en aplicaciones Java. En este laboratorio (LabEx), exploraremos el concepto de espías (spies) en Mockito. Un Espía es una herramienta de prueba única que actúa como un simulacro parcial: registra las interacciones con un objeto mientras permite que se llamen a los métodos reales.

Este doble comportamiento hace que los espías sean especialmente útiles cuando queremos verificar las interacciones con objetos pero aún necesitamos que se ejecute la funcionalidad real. A lo largo de este laboratorio, aprenderemos cómo crear espías utilizando diferentes enfoques, entenderemos cómo configurar el comportamiento de un espía y compararemos los espías con los simulacros (mocks) para resaltar sus diferencias clave.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL java(("Java")) -.-> java/ObjectOrientedandAdvancedConceptsGroup(["Object-Oriented and Advanced Concepts"]) java(("Java")) -.-> java/StringManipulationGroup(["String Manipulation"]) java(("Java")) -.-> java/DataStructuresGroup(["Data Structures"]) java/StringManipulationGroup -.-> java/strings("Strings") java/StringManipulationGroup -.-> java/stringbuffer_stringbuilder("StringBuffer/StringBuilder") java/DataStructuresGroup -.-> java/arrays("Arrays") java/DataStructuresGroup -.-> java/arrays_methods("Arrays Methods") java/DataStructuresGroup -.-> java/collections_methods("Collections Methods") java/ObjectOrientedandAdvancedConceptsGroup -.-> java/classes_objects("Classes/Objects") java/ObjectOrientedandAdvancedConceptsGroup -.-> java/arraylist("ArrayList") java/ObjectOrientedandAdvancedConceptsGroup -.-> java/annotation("Annotation") subgraph Lab Skills java/strings -.-> lab-117989{{"Espía (Spy) en Mockito"}} java/stringbuffer_stringbuilder -.-> lab-117989{{"Espía (Spy) en Mockito"}} java/arrays -.-> lab-117989{{"Espía (Spy) en Mockito"}} java/arrays_methods -.-> lab-117989{{"Espía (Spy) en Mockito"}} java/collections_methods -.-> lab-117989{{"Espía (Spy) en Mockito"}} java/classes_objects -.-> lab-117989{{"Espía (Spy) en Mockito"}} java/arraylist -.-> lab-117989{{"Espía (Spy) en Mockito"}} java/annotation -.-> lab-117989{{"Espía (Spy) en Mockito"}} end

Creación de un proyecto Java y comprensión del Espía (Spy) de Mockito

En este paso, configuraremos la estructura de nuestro proyecto y entenderemos qué es un Espía (Spy) en Mockito. Los Espías nos permiten registrar las llamadas a métodos en objetos reales mientras seguimos utilizando su implementación real.

Crear el archivo Java

Primero, creemos nuestro archivo Java principal para este laboratorio (LabEx). En el WebIDE, crea un nuevo archivo llamado MockitoSpyDemo.java en el directorio ~/project.

Crear un nuevo archivo

Agrega el siguiente código para importar las dependencias necesarias:

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.");
    }
}

Comprensión del Espía (Spy) de Mockito

Un Espía (Spy) en Mockito es un envoltorio (wrapper) alrededor de un objeto real que te permite:

  1. Registrar todas las interacciones con el objeto (como un simulacro - mock)
  2. Ejecutar los métodos reales del objeto (a diferencia de un simulacro - mock)

Puedes pensar en un espía como un observador que ve lo que le sucede a un objeto pero no interfiere con su comportamiento normal a menos que se le haya programado específicamente para hacerlo.

Vamos a agregar nuestro primer método de prueba para demostrar el uso básico de un espía. Agrega el siguiente método a tu clase 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());
}

Compilemos y ejecutemos nuestro código para ver si 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
Ejecutar el código

Deberías ver la siguiente salida:

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

Nuestro método principal se ejecuta correctamente. Sin embargo, para ejecutar las pruebas necesitamos utilizar JUnit. En lugar de configurar un complejo ejecutor (runner) de JUnit para este laboratorio, nos centraremos en entender la estructura del código y los conceptos.

En el siguiente paso, exploraremos diferentes formas de crear espías en Mockito.

Diferentes formas de crear Espías (Spies) en Mockito

En este paso, exploraremos diferentes enfoques para crear espías (spies) en Mockito. Aprenderemos cómo crear espías utilizando el método Mockito.spy() y la anotación @Spy.

Utilizando el método Mockito.spy()

La forma más directa de crear un espía es utilizando el método Mockito.spy(). Vamos a agregar un método de prueba que demuestre este enfoque:

@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());
}

Utilizando la anotación @Spy

Otra forma de crear un espía es utilizando la anotación @Spy. Este enfoque es especialmente útil cuando se desea inicializar los espías automáticamente. Agrega el siguiente código a tu clase 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());
}

Comprendiendo la diferencia entre los enfoques

Ambos enfoques crean un espía, pero tienen diferentes casos de uso:

  1. Mockito.spy() es más flexible y se puede utilizar en cualquier parte de tus métodos de prueba.
  2. La anotación @Spy es más conveniente cuando tienes muchos espías y deseas inicializarlos todos a la vez.

El método MockitoAnnotations.initMocks(this) es responsable de inicializar todos los campos anotados. En proyectos reales, es posible que uses @RunWith(MockitoJUnitRunner.class) para que esta inicialización se realice automáticamente.

Compilémos nuestro código actualizado:

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

El código debería compilar sin errores. Nuestros métodos de prueba ya están listos, pero como se mencionó anteriormente, necesitaríamos un ejecutor (runner) de JUnit para ejecutarlos automáticamente.

En el siguiente paso, aprenderemos cómo configurar (stub) los espías para anular su comportamiento predeterminado.

Configuración (Stubbing) de Espías y Manejo de Excepciones

En este paso, aprenderemos cómo configurar (stub) un espía (spy) para anular su comportamiento predeterminado y entenderemos cómo manejar las excepciones que pueden ocurrir al utilizar Mockito.

Configuración (Stubbing) de un Espía

Los espías utilizan los métodos de los objetos reales por defecto, pero a veces queremos cambiar este comportamiento con fines de prueba. Esto se llama "configurar (stub)" el espía. Vamos a agregar un método de prueba que demuestre cómo configurar un espía:

@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 la configuración (Stubbing)

Al configurar (stub) métodos de espías, se recomienda utilizar doReturn(), doThrow(), doAnswer(), etc., en lugar de when(). Esto se debe a que con los espías, la sintaxis when().thenReturn() puede llamar al método real durante la configuración (stubbing), lo que puede causar efectos secundarios inesperados.

Por ejemplo:

// 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

Manejo de la excepción NotAMockException

Si intentas utilizar los métodos de verificación de Mockito en un objeto normal (no un simulacro - mock o espía - spy), obtendrás una excepción NotAMockException. Vamos a agregar un método de prueba para demostrar esta excepción:

@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"));
    }
}

Compilémos nuestro código actualizado:

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

El código debería compilar sin errores. En este paso, aprendimos cómo configurar (stub) métodos de espías para anular su comportamiento predeterminado y cómo manejar la excepción NotAMockException que se produce al utilizar métodos de verificación en objetos normales.

En el siguiente paso, compararemos simulacros (mocks) y espías (spies) para entender sus diferencias clave.

Simulacro (Mock) vs Espía (Spy): Entendiendo las Diferencias

En este paso, compararemos directamente los simulacros (mocks) y los espías (spies) para entender sus diferencias clave. Esta comparación te ayudará a elegir la herramienta adecuada para tus necesidades de prueba.

Creando una Prueba de Comparación

Vamos a agregar un método de prueba que demuestre las diferencias entre simulacros (mocks) y espías (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());
}

Entendiendo las Diferencias Clave

Resumamos las diferencias clave entre simulacros (mocks) y espías (spies) en Mockito:

  1. Comportamiento de los Métodos:

    • Simulacro (Mock): Todos los métodos devuelven valores predeterminados (null, 0, false) a menos que se configuren (stub).
    • Espía (Spy): Los métodos llaman a la implementación real a menos que se configuren (stub).
  2. Estado del Objeto:

    • Simulacro (Mock): Las operaciones no cambian el estado (por ejemplo, agregar a una lista simulada no agrega realmente nada).
    • Espía (Spy): Las operaciones cambian el estado del objeto al igual que un objeto real.
  3. Creación:

    • Simulacro (Mock): Se crea a partir de una clase, no de una instancia (Mockito.mock(ArrayList.class)).
    • Espía (Spy): Por lo general, se crea a partir de una instancia (Mockito.spy(new ArrayList<>())).
  4. Casos de Uso:

    • Simulacro (Mock): Cuando quieres controlar completamente el comportamiento y no necesitas métodos reales.
    • Espía (Spy): Cuando quieres usar métodos reales pero necesitas anular solo algunos comportamientos.

Ejemplo Práctico: Cuándo Usar un Espía

Vamos a agregar una prueba más que muestra un caso de uso común para los espías (spies) - configurar parcialmente un objeto complejo:

@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());
}

Compilémos nuestro código actualizado:

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

El código debería compilar sin errores. En este paso, aprendimos las diferencias clave entre simulacros (mocks) y espías (spies), y cuándo usar cada uno. Los simulacros (mocks) reemplazan completamente el comportamiento del objeto original, mientras que los espías (spies) mantienen el comportamiento original pero te permiten anular métodos específicos cuando sea necesario.

Resumen

En este laboratorio, exploramos el concepto de espías (spies) en Mockito, una poderosa característica para pruebas unitarias en Java. Estos son los puntos clave que cubrimos:

  1. Creación de Espías (Spies): Aprendimos cómo crear espías utilizando tanto el método Mockito.spy() como la anotación @Spy. El primero es más flexible, mientras que el segundo es más conveniente cuando se tienen muchos espías.

  2. Comportamiento de los Espías (Spies): Los espías registran las llamadas a métodos como los simulacros (mocks), pero ejecutan los métodos reales por defecto. Esta doble naturaleza los hace especialmente útiles para probar objetos reales mientras se mantiene la capacidad de verificar interacciones.

  3. Configuración (Stubbing) de Espías (Spies): Descubrimos cómo anular el comportamiento predeterminado de los métodos de los espías utilizando doReturn(), doThrow() y otros métodos de configuración (stubbing). Esto nos permite controlar comportamientos específicos mientras se mantienen otros intactos.

  4. Manejo de Excepciones: Exploramos la excepción NotAMockException que se produce cuando se intentan utilizar métodos de verificación de Mockito en objetos normales, y cómo manejarla adecuadamente.

  5. Simulacro (Mock) vs Espía (Spy): Comparamos simulacros (mocks) y espías (spies) para entender sus diferencias clave. Los simulacros (mocks) reemplazan completamente el comportamiento original con respuestas predeterminadas o configuradas (stubbed), mientras que los espías (spies) mantienen el comportamiento original pero permiten anular métodos específicos.

Los espías (spies) son una poderosa herramienta en el arsenal de Mockito, ofreciendo un equilibrio entre el control total de los simulacros (mocks) y la realismo de los objetos reales. Son especialmente útiles cuando:

  • Se desea probar un objeto real pero se necesita verificar ciertas interacciones.
  • Se necesita anular solo métodos específicos mientras se mantiene el resto del comportamiento del objeto intacto.
  • Se está trabajando con objetos complejos donde simular todos los métodos sería poco práctico.

Al entender cuándo y cómo utilizar espías (spies), se pueden escribir pruebas unitarias más efectivas y mantenibles para aplicaciones Java.