Spy dans Mockito

JavaBeginner
Pratiquer maintenant

Introduction

Mockito est un puissant framework Java utilisé pour le mock et les tests unitaires d'applications Java. Dans ce laboratoire (LabEx), nous allons explorer le concept des spies dans Mockito. Un Spy est un outil de test unique qui agit comme un mock partiel - il suit les interactions avec un objet tout en permettant toujours d'appeler les méthodes réelles.

Ce comportement double rend les spies particulièrement utiles lorsque nous voulons vérifier les interactions avec des objets tout en ayant besoin que la fonctionnalité réelle s'exécute. Tout au long de ce laboratoire, nous apprendrons à créer des spies en utilisant différentes approches, à comprendre comment configurer le comportement d'un spy et à comparer les spies avec les mocks pour mettre en évidence leurs principales différences.

Création d'un projet Java et compréhension du Spy Mockito

Dans cette étape, nous allons configurer la structure de notre projet et comprendre ce qu'est un Spy dans Mockito. Les spies nous permettent de suivre les appels de méthodes sur des objets réels tout en utilisant leur implémentation réelle.

Créer le fichier Java

Commençons par créer notre fichier Java principal pour ce laboratoire (LabEx). Dans l'IDE Web, créez un nouveau fichier appelé MockitoSpyDemo.java dans le répertoire ~/project.

Créer un nouveau fichier

Ajoutez le code suivant pour importer les dépendances nécessaires :

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

Compréhension du Spy Mockito

Un Spy dans Mockito est un wrapper autour d'un objet réel qui vous permet de :

  1. Suivre toutes les interactions avec l'objet (comme un mock)
  2. Exécuter les méthodes réelles de l'objet (contrairement à un mock)

Imaginez un spy comme un observateur qui regarde ce qui se passe à un objet sans interférer avec son comportement normal, sauf s'il est spécifiquement programmé pour le faire.

Ajoutons notre première méthode de test pour démontrer l'utilisation de base d'un spy. Ajoutez la méthode suivante à votre 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());
}

Compilons et exécutons notre code pour voir s'il fonctionne :

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
Exécuter le code

Vous devriez voir la sortie suivante :

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

Notre méthode principale s'exécute correctement. Cependant, pour exécuter les tests, nous devons utiliser JUnit. Au lieu de configurer un exécuteur JUnit complexe pour ce laboratoire, nous allons nous concentrer sur la compréhension de la structure du code et des concepts.

Dans l'étape suivante, nous explorerons différentes façons de créer des spies dans Mockito.

Différentes façons de créer des spies dans Mockito

Dans cette étape, nous allons explorer différentes approches pour créer des spies dans Mockito. Nous apprendrons à créer des spies en utilisant la méthode Mockito.spy() et l'annotation @Spy.

Utilisation de la méthode Mockito.spy()

La façon la plus directe de créer un spy est d'utiliser la méthode Mockito.spy(). Ajoutons une méthode de test qui démontre cette approche :

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

Utilisation de l'annotation @Spy

Une autre façon de créer un spy est d'utiliser l'annotation @Spy. Cette approche est particulièrement utile lorsque vous souhaitez initialiser automatiquement les spies. Ajoutez le code suivant à votre 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());
}

Comprendre la différence entre les approches

Les deux approches créent un spy, mais elles ont des cas d'utilisation différents :

  1. Mockito.spy() est plus flexible et peut être utilisé n'importe où dans vos méthodes de test.
  2. L'annotation @Spy est plus pratique lorsque vous avez de nombreux spies et que vous souhaitez les initialiser tous d'un coup.

La méthode MockitoAnnotations.initMocks(this) est responsable de l'initialisation de tous les champs annotés. Dans les projets réels, vous pourriez utiliser @RunWith(MockitoJUnitRunner.class) pour que cette initialisation soit effectuée automatiquement.

Compilons notre code mis à jour :

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

Le code devrait compiler sans erreur. Nos méthodes de test sont maintenant prêtes, mais comme mentionné précédemment, nous aurions besoin d'un exécuteur JUnit pour les exécuter automatiquement.

Dans l'étape suivante, nous apprendrons à configurer (stub) les spies pour remplacer leur comportement par défaut.

Simulation (stubbing) de spies et gestion des exceptions

Dans cette étape, nous apprendrons à configurer (stub) un spy pour remplacer son comportement par défaut et à gérer les exceptions qui peuvent survenir lors de l'utilisation de Mockito.

Configuration (Stubbing) d'un spy

Les spies utilisent les méthodes des objets réels par défaut, mais parfois, nous souhaitons changer ce comportement à des fins de test. Cela s'appelle "configurer (stub)" le spy. Ajoutons une méthode de test qui montre comment configurer un 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);
}

Note importante sur la configuration (stubbing)

Lors de la configuration des méthodes d'un spy, il est recommandé d'utiliser doReturn(), doThrow(), doAnswer(), etc., au lieu de when(). En effet, avec les spies, la syntaxe when().thenReturn() peut appeler la méthode réelle lors de la configuration, ce qui peut entraîner des effets secondaires inattendus.

Par exemple :

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

Gestion de l'exception NotAMockException

Si vous essayez d'utiliser les méthodes de vérification de Mockito sur un objet normal (pas un mock ou un spy), vous obtiendrez une NotAMockException. Ajoutons une méthode de test pour démontrer cette exception :

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

Compilons notre code mis à jour :

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

Le code devrait compiler sans erreur. Dans cette étape, nous avons appris à configurer les méthodes des spies pour remplacer leur comportement par défaut et à gérer l'exception NotAMockException qui se produit lorsque nous utilisons les méthodes de vérification sur des objets normaux.

Dans l'étape suivante, nous comparerons les mocks et les spies pour comprendre leurs principales différences.

Mock vs Spy : Comprendre les différences

Dans cette étape, nous comparerons directement les mocks et les spies pour comprendre leurs principales différences. Cette comparaison vous aidera à choisir l'outil adapté à vos besoins de test.

Création d'un test de comparaison

Ajoutons une méthode de test qui démontre les différences entre les mocks et les 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());
}

Comprendre les principales différences

Récapitulons les principales différences entre les mocks et les spies dans Mockito :

  1. Comportement des méthodes :

    • Mock : Toutes les méthodes retournent des valeurs par défaut (null, 0, false) à moins d'être configurées (stubbed)
    • Spy : Les méthodes appellent l'implémentation réelle à moins d'être configurées (stubbed)
  2. État de l'objet :

    • Mock : Les opérations ne modifient pas l'état (par exemple, ajouter à une liste mock n'ajoute rien en réalité)
    • Spy : Les opérations modifient l'état de l'objet comme un objet réel
  3. Création :

    • Mock : Créé à partir d'une classe, pas d'une instance (Mockito.mock(ArrayList.class))
    • Spy : Habituellement créé à partir d'une instance (Mockito.spy(new ArrayList<>()))
  4. Cas d'utilisation :

    • Mock : Lorsque vous voulez contrôler complètement le comportement et que vous n'avez pas besoin des méthodes réelles
    • Spy : Lorsque vous voulez utiliser les méthodes réelles mais que vous avez besoin de remplacer seulement certains comportements

Exemple pratique : Quand utiliser un spy

Ajoutons un autre test qui montre un cas d'utilisation courant des spies - configurer partiellement un objet complexe :

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

Compilons notre code mis à jour :

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

Le code devrait compiler sans erreur. Dans cette étape, nous avons appris les principales différences entre les mocks et les spies, et quand utiliser chacun d'eux. Les mocks remplacent complètement le comportement de l'objet original, tandis que les spies conservent le comportement original mais vous permettent de remplacer des méthodes spécifiques si nécessaire.

Résumé

Dans ce laboratoire (lab), nous avons exploré le concept des spies dans Mockito, une fonctionnalité puissante pour les tests unitaires en Java. Voici les points clés que nous avons abordés :

  1. Création de spies : Nous avons appris à créer des spies en utilisant à la fois la méthode Mockito.spy() et l'annotation @Spy. La première est plus flexible, tandis que la deuxième est plus pratique lorsque vous avez de nombreux spies.

  2. Comportement des spies : Les spies suivent les appels de méthodes comme les mocks, mais exécutent les méthodes réelles par défaut. Cette double nature les rend particulièrement utiles pour tester des objets réels tout en conservant la capacité de vérifier les interactions.

  3. Configuration (Stubbing) des spies : Nous avons découvert comment remplacer le comportement par défaut des méthodes des spies en utilisant doReturn(), doThrow() et d'autres méthodes de configuration (stubbing). Cela nous permet de contrôler des comportements spécifiques tout en conservant les autres intacts.

  4. Gestion des exceptions : Nous avons exploré l'exception NotAMockException qui se produit lorsque l'on essaie d'utiliser les méthodes de vérification de Mockito sur des objets normaux, et comment la gérer correctement.

  5. Mock vs Spy : Nous avons comparé les mocks et les spies pour comprendre leurs principales différences. Les mocks remplacent complètement le comportement original par des réponses par défaut ou configurées (stubbed), tandis que les spies conservent le comportement original mais permettent de remplacer des méthodes spécifiques.

Les spies sont un outil puissant dans l'arsenal de Mockito, offrant un équilibre entre le contrôle total des mocks et le réalisme des objets réels. Ils sont particulièrement utiles lorsque :

  • Vous voulez tester un objet réel mais avez besoin de vérifier certaines interactions
  • Vous avez besoin de remplacer seulement des méthodes spécifiques tout en conservant le reste du comportement de l'objet intact
  • Vous travaillez avec des objets complexes pour lesquels mocker toutes les méthodes serait impraticable

En comprenant quand et comment utiliser les spies, vous pouvez écrire des tests unitaires plus efficaces et maintenables pour vos applications Java.