Fonctionnalités Java 8 de Mockito

Fonctionnalités Java 8 de Mockito

1. Vue d'ensemble

Java 8 a introduit une gamme de nouvelles fonctionnalités impressionnantes, telles que lambda et les flux. Et naturellement, Mockito a mis à profit ces innovations récentes dans ses2nd major version.

Dans cet article, nous allons explorer tout ce que cette combinaison puissante a à offrir.

2. Interface moqueuse avec une méthode par défaut

À partir de Java 8, nous pouvons maintenant écrire des implémentations de méthodes dans nos interfaces. Cela pourrait être une grande nouvelle fonctionnalité, mais son introduction au langage violait un concept fort qui faisait partie de Java depuis sa conception.

La version 1 de Mockito n'était pas prête pour ce changement. En gros, parce que cela ne nous permettait pas de lui demander d’appeler des méthodes réelles à partir d’interfaces.

Imaginez que nous ayons une interface avec 2 déclarations de méthode: la première est la signature de méthode à l’ancienne à laquelle nous sommes tous habitués, et l’autre est une toute nouvelle méthodedefault:

public interface JobService {

    Optional findCurrentJobPosition(Person person);

    default boolean assignJobPosition(Person person, JobPosition jobPosition) {
        if(!findCurrentJobPosition(person).isPresent()) {
            person.setCurrentJobPosition(jobPosition);

            return true;
        } else {
            return false;
        }
    }
}

Notez que la méthodeassignJobPosition()default a un appel à la méthodefindCurrentJobPosition() non implémentée.

Maintenant, supposons que nous voulions tester notre implémentation deassignJobPosition() sans écrire une implémentation réelle defindCurrentJobPosition(). Nous pourrions simplement créer une version simulée deJobService, puis dire à Mockito de renvoyer une valeur connue de l'appel à notre méthode non implémentée et appeler la méthode réelle lorsqueassignJobPosition() est appelé:

public class JobServiceUnitTest {

    @Mock
    private JobService jobService;

    @Test
    public void givenDefaultMethod_whenCallRealMethod_thenNoExceptionIsRaised() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(person))
              .thenReturn(Optional.of(new JobPosition()));

        doCallRealMethod().when(jobService)
          .assignJobPosition(
            Mockito.any(Person.class),
            Mockito.any(JobPosition.class)
        );

        assertFalse(jobService.assignJobPosition(person, new JobPosition()));
    }
}

C’est parfaitement raisonnable et cela fonctionnerait parfaitement si nous utilisions une classe abstraite au lieu d’une interface.

Cependant, le fonctionnement interne de Mockito 1 n’était tout simplement pas prêt pour cette structure. Si nous devions exécuter ce code avec Mockito avant la version 2, nous obtiendrions cette erreur bien décrite:

org.mockito.exceptions.base.MockitoException:
Cannot call a real method on java interface. The interface does not have any implementation!
Calling real methods is only possible when mocking concrete classes.

Mockito fait son travail et nous dit qu'il ne peut pas appeler de vraies méthodes sur les interfaces puisque cette opération était impensable avant Java 8.

La bonne nouvelle est qu'en changeant simplement la version de Mockito que nous utilisons, nous pouvons faire disparaître cette erreur. En utilisant Maven, par exemple, nous pourrions utiliser la version 2.7.5 (la dernière version de Mockito peut être trouvéehere):


    org.mockito
    mockito-core
    2.7.5
    test

Il n'est pas nécessaire de modifier le code. La prochaine fois que nous lancerons notre test, l'erreur ne se produira plus.

3. Renvoie les valeurs par défaut pourOptional etStream

Optional etStream sont d'autres nouveaux ajouts à Java 8. Une similitude entre les deux classes est que les deux ont un type spécial de valeur qui représente un objet vide. Cet objet vide permet d'éviter plus facilement lesNullPointerException. jusqu'ici omniprésents

3.1. Exemple avecOptional

Considérons un service qui injecte lesJobService décrits dans la section précédente et a une méthode qui appelleJobService#findCurrentJobPosition():

public class UnemploymentServiceImpl implements UnemploymentService {

    private JobService jobService;

    public UnemploymentServiceImpl(JobService jobService) {
        this.jobService = jobService;
    }

    @Override
    public boolean personIsEntitledToUnemploymentSupport(Person person) {
        Optional optional = jobService.findCurrentJobPosition(person);

        return !optional.isPresent();
    }
}

Supposons maintenant que nous souhaitons créer un test pour vérifier que, lorsqu'une personne n'a pas de poste à pourvoir, elle a droit à une aide au chômage.

Dans ce cas, nous forcerionsfindCurrentJobPosition() à renvoyer unOptional vide. Before Mockito 2, nous avons dû nous moquer de l'appel à cette méthode:

public class UnemploymentServiceImplUnitTest {

    @Mock
    private JobService jobService;

    @InjectMocks
    private UnemploymentServiceImpl unemploymentService;

    @Test
    public void givenReturnIsOfTypeOptional_whenMocked_thenValueIsEmpty() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(any(Person.class)))
          .thenReturn(Optional.empty());

        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

Cette instructionwhen(…).thenReturn(…) à la ligne 13 est nécessaire car la valeur de retour par défaut de Mockito pour tout appel de méthode à un objet simulé estnull. La version 2 a changé ce comportement.

Puisque nous gérons rarement les valeurs nulles lorsque nous traitons avecOptional,Mockito now returns an empty Optional by default. C'est exactement la même valeur que le retour d'un appel àOptional.empty().

Donc,when using Mockito version 2, nous pourrions nous débarrasser de la ligne 13 et notre test réussirait toujours:

public class UnemploymentServiceImplUnitTest {

    @Test
    public void givenReturnIsOptional_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();

        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

3.2. Exemple avecStream

Le même comportement se produit lorsque nous nous moquons d'une méthode qui renvoie unStream.

Ajoutons une nouvelle méthode à notre interfaceJobService qui renvoie un Stream représentant tous les postes de travail pour lesquels une personne a déjà travaillé:

public interface JobService {
    Stream listJobs(Person person);
}

Cette méthode est utilisée sur une autre nouvelle méthode qui demande si une personne a déjà travaillé sur un travail correspondant à une chaîne de recherche donnée:

public class UnemploymentServiceImpl implements UnemploymentService {

    @Override
    public Optional searchJob(Person person, String searchString) {
        return jobService.listJobs(person)
          .filter((j) -> j.getTitle().contains(searchString))
          .findFirst();
    }
}

Donc, supposons que nous voulions tester correctement l'implémentation desearchJob(), sans avoir à nous soucier d'écrire leslistJobs() et supposons que nous voulons tester le scénario lorsque la personne n'a encore travaillé à aucun travail. Dans ce cas, nous voudrions quelistJobs() renvoie unStream vide.

Before Mockito 2, we would need to mock the call to listJobs() pour écrire un tel test:

public class UnemploymentServiceImplUnitTest {

    @Test
    public void givenReturnIsOfTypeStream_whenMocked_thenValueIsEmpty() {
        Person person = new Person();
        when(jobService.listJobs(any(Person.class))).thenReturn(Stream.empty());

        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

If we upgrade to version 2, nous pourrions abandonner l'appelwhen(…).thenReturn(…), car maintenantMockito will return an empty Stream on mocked methods by default:

public class UnemploymentServiceImplUnitTest {

    @Test
    public void givenReturnIsStream_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();

        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

4. Tirer parti des expressions Lambda

Avec les expressions lambda de Java 8, nous pouvons rendre les instructions beaucoup plus compactes et plus faciles à lire. Lorsque vous travaillez avec Mockito, 2 très bons exemples de la simplicité apportée par les expressions lambda sontArgumentMatchers et customAnswers.

4.1. Combinaison de Lambda etArgumentMatcher

Avant Java 8, nous devions créer une classe qui implémentaitArgumentMatcher et écrire notre règle personnalisée dans la méthodematches().

Avec Java 8, nous pouvons remplacer la classe interne par une simple expression lambda:

public class ArgumentMatcherWithLambdaUnitTest {

    @Test
    public void whenPersonWithJob_thenIsNotEntitled() {
        Person peter = new Person("Peter");
        Person linda = new Person("Linda");

        JobPosition teacher = new JobPosition("Teacher");

        when(jobService.findCurrentJobPosition(
          ArgumentMatchers.argThat(p -> p.getName().equals("Peter"))))
          .thenReturn(Optional.of(teacher));

        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(linda));
        assertFalse(unemploymentService.personIsEntitledToUnemploymentSupport(peter));
    }
}

4.2. Combinaison de Lambda et deAnswerpersonnalisés

Le même effet peut être obtenu en combinant des expressions lambda avec lesAnswer de Mockito.

Par exemple, si nous voulions simuler des appels à la méthodelistJobs() afin de lui faire retourner unStream contenant un seulJobPosition si le nom dePerson est «Peter », Et unStream vide sinon, nous devrons créer une classe (anonyme ou interne) qui implémente l'interfaceAnswer.

Encore une fois, l'utilisation d'une expression lambda, nous permet d'écrire tout le comportement fictif en ligne:

public class CustomAnswerWithLambdaUnitTest {

    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);

        when(jobService.listJobs(any(Person.class))).then((i) ->
          Stream.of(new JobPosition("Teacher"))
          .filter(p -> ((Person) i.getArgument(0)).getName().equals("Peter")));
    }
}

Notez que, dans l'implémentation ci-dessus, il n'y a pas besoin de la classe internePersonAnswer.

5. Conclusion

Dans cet article, nous avons expliqué comment exploiter conjointement les nouvelles fonctionnalités de Java 8 et de Mockito 2 pour écrire un code plus propre, plus simple et plus court. Si vous ne connaissez pas certaines des fonctionnalités de Java 8 que nous avons vues ici, consultez certains de nos articles:

Vérifiez également le code d'accompagnement sur nosGitHub repository.