web-dev-qa-db-fra.com

La simulation du service avec Mockito 2 conduit à une erreur de talonnage

J'essaie de simuler le comportement d'une classe en utilisant Mockito . Cela a fonctionné avec Mockito 1.x. Migration vers JUnit 5 et Mockito 2, cela ne semble plus fonctionner.

@ExtendWith(MockitoExtension.class)
public class MockitoExample {

  static abstract class TestClass {
    public abstract int booleanMethod(boolean arg);
  }

  @Mock
  TestClass testClass;

  @BeforeEach
  public void beforeEach() {
    when(testClass.booleanMethod(eq(true))).thenReturn(1);
    when(testClass.booleanMethod(eq(false))).thenReturn(2);
  }

  @Test
  public void test() {
    assertEquals(1,testClass.booleanMethod(true));
    assertEquals(2,testClass.booleanMethod(false));
  }
}

On s'attend à ce que TestClass simulé montre le comportement tel que testé dans la méthode de test.

L'erreur que je reçois est:

org.mockito.exceptions.misusing.PotentialStubbingProblem: 

  Strict stubbing argument mismatch. Please check:
   - this invocation of 'booleanMethod' method:
      testClass.booleanMethod(false);
      -> at org.oneandone.ejbcdiunit.mockito_example.MockitoExample.beforeEach(MockitoExample.Java:30)
   - has following stubbing(s) with different arguments:
      1. testClass.booleanMethod(false);
        -> at org.oneandone.ejbcdiunit.mockito_example.MockitoExample.beforeEach(MockitoExample.Java:29)
  Typically, stubbing argument mismatch indicates user mistake when writing tests.
  Mockito fails early so that you can debug potential problem easily.
  However, there are legit scenarios when this exception generates false negative signal:
    - stubbing the same method multiple times using 'given().will()' or 'when().then()' API
      Please use 'will().given()' or 'doReturn().when()' API for stubbing.
    - stubbed method is intentionally invoked with different arguments by code under test
      Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).
  For more information see javadoc for PotentialStubbingProblem class.

Dans les deux cas, l'argument false semble être apparié, bien que j'aie clairement apparié avec true.

Est-ce un bug dans Mockito 2.17 ou un malentendu? Comment dois-je/puis-je utiliser Mockito 2.x pour simuler des appels avec différents arguments booléens?

Le exemple peut également être trouvé sur github. Mais surefire commencera le test en utilisant uniquement 

mvn test -Dtest=MockitoExample

L'exécution du test à l'aide de Mockito 2.21 conduit aux mêmes résultats. 

4
aschoerk

Avec des stubs stricts (comportement par défaut de Mockito), appeler plusieurs whens sur la même méthode réinitialisera cette simulation. La solution consiste à appeler when une fois et à avoir la logique dans un Answer:

@BeforeEach
public void beforeEach() {
    when(testClass.booleanMethod(anyBoolean())).thenAnswer(invocationOnMock -> {
        if ((boolean) invocationOnMock.getArguments()[0]) {
            return 1;
        }
        return 2;
    });
}

Alternativement, vous pouvez utiliser des moqueries indulgentes, mais ce n’est pas toujours une bonne idée, car elles vous permettent d’aggraver les choses et vous permettent de faire plus facilement des erreurs lors de votre test, ce qui peut entraîner des bogues inaperçus dans le code "de production":

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class MockitoExample {
7
Mureinik

Depuis Mockito 2.20, il est également possible d’ajouter localement ()

@ExtendWith(MockitoExtension.class)
public class MockitoExample {

  static abstract class TestClass {
    public abstract int booleanMethod(boolean arg);
  }

  @Mock
  TestClass testClass;

  @BeforeEach
  public void beforeEach() {
    lenient().when(testClass.booleanMethod(eq(true))).thenReturn(1);
    lenient().when(testClass.booleanMethod(eq(false))).thenReturn(2);
  }

  @Test
  public void test() {
    assertEquals(1,testClass.booleanMethod(true));
    assertEquals(2,testClass.booleanMethod(false));
  }
}
4
aschoerk

Les Mockito 1 et 2 n'ont pas le même niveau de "sévérité".
Outre l'utilisation de Mockito 2 avec JUnit 4 ou 5, le niveau par défaut sera toujours différent. 

Pour résumer :

3 niveaux de rigueur: 

  • LENIENT: rigueur minimale
  • WARN: avertissements supplémentaires émis sur la console
  • STRICT_STUBS: garantit des tests clairs en générant une exception en cas de mauvaise utilisation, mais peut également produire des faux positifs.

Niveau effectif par défaut en fonction des API utilisées: 

  • Mockito 1: LENIENT 
  • Mockito 2 avec JUnit 4: WARN
  • Mockito 2 avec JUnit 5 (MockitoExtension.class): STRICT_STUBS 
  • Mockito 3: prévu pour être STRICT_STUBS.

Plus de détails

La documentation de Mockito est très claire à ce sujet:

Le Strictness javadoc déclare: 

Configure la "rigueur" de Mockito lors d'une session moqueuse.A session correspond généralement à une seule invocation de méthode de test. Rigueur conduit des tests plus propres et une meilleure productivité.La manière la plus simple de Exploitation améliorée Strictness utilise le support JUnit de Mockito (MockitoRule ou MockitoJUnitRunner) .Si vous ne pouvez pas utiliser le support JUnit MockitoSession est la voie à suivre.

Comment le niveau de rigueur influence-t-il le comportement du test (session moqueuse )?

1 .Strictness.LENIENT - pas de comportement ajouté. Paramètre par défaut de Mockito 1.x.Recommandé uniquement si vous ne pouvez pas utiliser STRICT_STUBS ni WARN.

2 .Strictness.WARN - aide à garder les tests propres et à améliorer la capacité de débogage.Reporte les avertissements de la console concernant les talons et talons inutilisés Argument mismatch (voir org.mockito.quality.MockitoHint) .La valeur par défaut comportement de Mockito 2.x lorsque JUnitRule ou MockitoJUnitRunner sont utilisés . Recommandé si vous ne pouvez pas utiliser STRICT_STUBS.

3 .Strictness.STRICT_STUBS - garantit des tests clairs, réduit la duplication du code de test, améliore la capacité de débogage. Meilleure combinaison de flexibilité et la productivité. Fortement recommandé.Planifié par défaut pour Mockito v3.Voir STRICT_STUBS pour plus de détails.

Mais quelle que soit l'exception levée associée au message 

"a stubbing suivant avec des arguments différents"

semble être un contrôle excessivement strict. Le message d'exception prouve que d'une certaine manière: 

Cependant, il existe des scénarios légitimes lorsque cette exception génère false signal négatif:

...

  • la méthode stubbed est invoquée intentionnellement avec différents arguments par le code sous test

Donc, l'interdire par défaut semble être trop.
Donc, si vous utilisez JUnit 5, vous pouvez utiliser WARNING comme alternative à STRICT_STUBS mais vous voulez généralement éviter que LENIENT soit trop silencieux. 

En plus de MockitoExtension, la bibliothèque mockito-junit-jupiter fournit @MockitoSettings qui peut être utilisée au niveau de la méthode ainsi qu'au niveau de la classe. 

Voici un exemple : 

import Java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

@ExtendWith(MockitoExtension.class)
public class FooTest {

    @MockitoSettings(strictness = Strictness.WARN)
    @Test
    void foo() throws Exception {
        List<String> strings = Mockito.mock(List.class);
        Mockito.when(strings.add("a"))
               .thenReturn(true);
        Mockito.when(strings.add("b"))
               .thenReturn(false);
    }

    @Test
    void fooKo() throws Exception {
        List<String> strings = Mockito.mock(List.class);
        Mockito.when(strings.add("a"))
               .thenReturn(true);
        Mockito.when(strings.add("b"))
               .thenReturn(false);

    }

}

fooKo() lève l'exception Mockito de mauvaise utilisation pendant que foo() réussit, mais fournit des avertissements utiles: 

 [MockitoHint] FooTest (voir javadoc pour MockitoHint): 
 [MockitoHint] 1. Inutilisé -> sur FooTest.foo (FooTest.Java:19) 
 [MockitoHint] 2. Inutilisé -> sur FooTest. foo (FooTest.Java:21) 

Comme autre alternative, vous pouvez également utiliser Mockito.lenient() très bien décrit par Aschoerk pour appliquer la rigueur indulgente à une invocation spécifique. Ainsi, vous pouvez définir chaque appel simulé comme indulgent lors de l’instanciation simulée:

@Test
void foo() throws Exception {
    List<String> strings = Mockito.mock(List.class, Mockito.withSettings()
                                                           .lenient());
     ....
}
0
davidxxx