Initialisation des objets fictifs - MockIto


122

Il existe de nombreuses façons d'initialiser un objet fictif à l'aide de MockIto. Quel est le meilleur moyen parmi ceux-ci?

1.

 public class SampleBaseTestCase {

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

2.

@RunWith(MockitoJUnitRunner.class)

[MODIFIER] 3.

mock(XXX.class);

suggérez-moi s'il y a d'autres moyens mieux que ceux-ci ...

Réponses:


153

Pour l'initialisation fictive , l'utilisation du runner ou du MockitoAnnotations.initMockssont des solutions strictement équivalentes. Depuis le javadoc du MockitoJUnitRunner :

JUnit 4.5 runner initializes mocks annotated with Mock, so that explicit usage of MockitoAnnotations.initMocks(Object) is not necessary. Mocks are initialized before each test method.


La première solution (avec le MockitoAnnotations.initMocks) peut être utilisée lorsque vous avez déjà configuré un runner spécifique ( SpringJUnit4ClassRunnerpar exemple) sur votre cas de test.

La deuxième solution (avec le MockitoJUnitRunner) est la plus classique et ma préférée. Le code est plus simple. L'utilisation d'un runner offre le grand avantage de la validation automatique de l'utilisation du framework (décrite par @David Wallace dans cette réponse ).

Les deux solutions permettent de partager les simulacres (et les espions) entre les méthodes de test. Couplés au @InjectMocks, ils permettent d'écrire des tests unitaires très rapidement. Le code de simulation standard est réduit, les tests sont plus faciles à lire. Par exemple:

@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest {

    @Mock private ArticleCalculator calculator;
    @Mock(name = "database") private ArticleDatabase dbMock;
    @Spy private UserProvider userProvider = new ConsumerUserProvider();

    @InjectMocks private ArticleManager manager;

    @Test public void shouldDoSomething() {
        manager.initiateArticle();
        verify(database).addListener(any(ArticleListener.class));
    }

    @Test public void shouldDoSomethingElse() {
        manager.finishArticle();
        verify(database).removeListener(any(ArticleListener.class));
    }
}

Avantages: le code est minimal

Inconvénients: magie noire. IMO, cela est principalement dû à l'annotation @InjectMocks. Avec cette annotation "vous perdez la douleur du code" (voir les bons commentaires de @Brice )


La troisième solution consiste à créer votre maquette sur chaque méthode de test. Il permet comme expliqué par @mlk dans sa réponse d'avoir un " test autonome ".

public class ArticleManagerTest {

    @Test public void shouldDoSomething() {
        // given
        ArticleCalculator calculator = mock(ArticleCalculator.class);
        ArticleDatabase database = mock(ArticleDatabase.class);
        UserProvider userProvider = spy(new ConsumerUserProvider());
        ArticleManager manager = new ArticleManager(calculator, 
                                                    userProvider, 
                                                    database);

        // when 
        manager.initiateArticle();

        // then 
        verify(database).addListener(any(ArticleListener.class));
    }

    @Test public void shouldDoSomethingElse() {
        // given
        ArticleCalculator calculator = mock(ArticleCalculator.class);
        ArticleDatabase database = mock(ArticleDatabase.class);
        UserProvider userProvider = spy(new ConsumerUserProvider());
        ArticleManager manager = new ArticleManager(calculator, 
                                                    userProvider, 
                                                    database);

        // when 
        manager.finishArticle();

        // then
        verify(database).removeListener(any(ArticleListener.class));
    }
}

Avantages: Vous démontrez clairement comment fonctionne votre API (BDD ...)

Inconvénients: il y a plus de code passe-partout. (La création de mocks)


Ma recommandation est un compromis. Utilisez l' @Mockannotation avec @RunWith(MockitoJUnitRunner.class), mais n'utilisez pas @InjectMocks:

@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest {

    @Mock private ArticleCalculator calculator;
    @Mock private ArticleDatabase database;
    @Spy private UserProvider userProvider = new ConsumerUserProvider();

    @Test public void shouldDoSomething() {
        // given
        ArticleManager manager = new ArticleManager(calculator, 
                                                    userProvider, 
                                                    database);

        // when 
        manager.initiateArticle();

        // then 
        verify(database).addListener(any(ArticleListener.class));
    }

    @Test public void shouldDoSomethingElse() {
        // given
        ArticleManager manager = new ArticleManager(calculator, 
                                                    userProvider, 
                                                    database);

        // when 
        manager.finishArticle();

        // then 
        verify(database).removeListener(any(ArticleListener.class));
    }
}

Avantages: Vous démontrez clairement comment fonctionne votre API (Comment my ArticleManagerest instancié). Pas de code passe-partout.

Inconvénients: le test n'est pas autonome, moins de problèmes de code


Attention cependant, les annotations sont utiles mais elles ne vous protègent pas pour créer une mauvaise conception d'OO (ou pour la dégrader). Personnellement, même si je suis heureux de réduire le code standard, je perds la douleur du code (ou PITA) qui est le déclencheur pour changer la conception en une meilleure, donc moi et l'équipe prêtons attention à la conception OO. Je pense que suivre la conception OO avec des principes comme la conception SOLIDE ou les idées du GOOS est beaucoup plus important que de choisir comment instancier des simulations.
Brice

1
(suite) Si vous ne voyez pas comment cet objet est créé, vous n'en ressentez pas la douleur, et les futurs programmeurs pourraient ne pas bien réagir si de nouvelles fonctionnalités devaient être ajoutées. Quoi qu'il en soit, c'est discutable dans les deux sens, je dis juste de faire attention à ce sujet.
Brice

6
Il n'est PAS CORRECT que ces deux soient équivalents. Il n'est PAS VRAI qu'un code plus simple soit le seul avantage à utiliser MockitoJUnitRunner. Pour plus d'informations sur les différences, consultez la question sur stackoverflow.com/questions/10806345/… et ma réponse.
Dawood ibn Kareem

2
@Gontard Oui, bien sûr, les dépendances sont visibles, mais j'ai vu du code mal tourner en utilisant cette approche. À propos de l'utilisation du Collaborator collab = mock(Collaborator.class), à mon avis, cette façon est certainement une approche valable. Bien que cela puisse avoir tendance à être verbeux, vous pouvez gagner en compréhensibilité et en refactorabilité des tests. Les deux méthodes ont leurs avantages et leurs inconvénients, je n'ai pas encore décidé quelle approche est la meilleure. Amyway, il est toujours possible d'écrire de la merde, et dépend probablement du contexte et du codeur.
Brice

1
@mlk je suis totalement d'accord avec vous. Mon anglais n'est pas très bon et il manque de nuances. Mon point était d'insister sur le mot UNIT.
gontard

30

Il existe maintenant (à partir de la v1.10.7) une quatrième façon d'instancier des simulations, qui utilise une règle JUnit4 appelée MockitoRule .

@RunWith(JUnit4.class)   // or a different runner of your choice
public class YourTest
  @Rule public MockitoRule rule = MockitoJUnit.rule();
  @Mock public YourMock yourMock;

  @Test public void yourTestMethod() { /* ... */ }
}

JUnit recherche les sous-classes de TestRule annotées avec @Rule et les utilise pour encapsuler les instructions de test fournies par le Runner . Le résultat est que vous pouvez extraire les méthodes @Before, @After, et même essayer ... de capturer les wrappers dans les règles. Vous pouvez même interagir avec ceux-ci à partir de votre test, comme le fait ExpectedException .

MockitoRule se comporte presque exactement comme MockitoJUnitRunner , sauf que vous pouvez utiliser n'importe quel autre runner, tel que Parameterized (qui permet à vos constructeurs de test de prendre des arguments afin que vos tests puissent être exécutés plusieurs fois), ou le test runner de Robolectric (ainsi son classloader peut fournir des remplacements Java pour les classes natives Android). Cela le rend strictement plus flexible à utiliser dans les versions récentes de JUnit et Mockito.

En résumé:

  • Mockito.mock(): Appel direct sans prise en charge des annotations ni validation de l'utilisation.
  • MockitoAnnotations.initMocks(this): Prise en charge des annotations, pas de validation d'utilisation.
  • MockitoJUnitRunner: Prise en charge des annotations et validation de l'utilisation, mais vous devez utiliser ce runner.
  • MockitoRule: Prise en charge des annotations et validation de l'utilisation avec n'importe quel runner JUnit.

Voir aussi: Comment fonctionne JUnit @Rule?


3
À Kotlin, la règle ressemble à ceci:@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
Cristan

10

Il existe une bonne façon de faire cela.

  • S'il s'agit d'un test unitaire, vous pouvez le faire:

    @RunWith(MockitoJUnitRunner.class)
    public class MyUnitTest {
    
        @Mock
        private MyFirstMock myFirstMock;
    
        @Mock
        private MySecondMock mySecondMock;
    
        @Spy
        private MySpiedClass mySpiedClass = new MySpiedClass();
    
        // It's gonna inject the 2 mocks and the spied object per reflection to this object
        // The java doc of @InjectMocks explains it really well how and when it does the injection
        @InjectMocks
        private MyClassToTest myClassToTest;
    
        @Test
        public void testSomething() {
        }
    }
  • EDIT: S'il s'agit d'un test d'intégration, vous pouvez le faire (ce n'est pas prévu pour être utilisé de cette façon avec Spring. Montrez simplement que vous pouvez initialiser des simulations avec différents Runners):

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("aplicationContext.xml")
    public class MyIntegrationTest {
    
        @Mock
        private MyFirstMock myFirstMock;
    
        @Mock
        private MySecondMock mySecondMock;
    
        @Spy
        private MySpiedClass mySpiedClass = new MySpiedClass();
    
        // It's gonna inject the 2 mocks and the spied object per reflection to this object
        // The java doc of @InjectMocks explains it really well how and when it does the injection
        @InjectMocks
        private MyClassToTest myClassToTest;
    
        @Before
        public void setUp() throws Exception {
              MockitoAnnotations.initMocks(this);
        }
    
        @Test
        public void testSomething() {
        }
    }

1
Si MOCK est également impliqué dans les tests d'intégration, cela aura-t-il un sens?
VinayVeluri

2
en fait ce ne sera pas, votre droit. Je voulais juste montrer les possibilités de Mockito. Par exemple, si vous utilisez RESTFuse, vous devez utiliser leur runner afin de pouvoir initialiser des simulations avec MockitoAnnotations.initMocks (this);
emd

8

MockitoAnnotations et le coureur ont été bien discutés ci-dessus, donc je vais ajouter ma tuppence pour les mal-aimés:

XXX mockedXxx = mock(XXX.class);

J'utilise ceci parce que je le trouve un peu plus descriptif et que je préfère (pas à droite interdire) les tests unitaires à ne pas utiliser de variables membres car j'aime que mes tests soient (autant qu'ils peuvent être) autonomes.


Y a-t-il un autre avantage par rapport à l'utilisation de mock (XX.class), sauf que le cas de test est autonome?
VinayVeluri

Pas pour autant que je sache.
Michael Lloyd Lee mlk

3
Moins de magie à comprendre pour lire le test. Vous déclarez la variable et lui donnez une valeur - pas d'annotations, de réflexion, etc.
Karu

2

Un petit exemple pour JUnit 5 Jupiter, le "RunWith" a été supprimé, vous devez maintenant utiliser les extensions en utilisant l'annotation "@ExtendWith".

@ExtendWith(MockitoExtension.class)
class FooTest {

  @InjectMocks
  ClassUnderTest test = new ClassUnderTest();

  @Spy
  SomeInject bla = new SomeInject();
}

0

Les autres réponses sont excellentes et contiennent plus de détails si vous les voulez / en avez besoin.
En plus de ceux-ci, je voudrais ajouter un TL; DR:

  1. Préfère utiliser
    • @RunWith(MockitoJUnitRunner.class)
  2. Si vous ne pouvez pas (parce que vous utilisez déjà un autre coureur), préférez utiliser
    • @Rule public MockitoRule rule = MockitoJUnit.rule();
  3. Similaire à (2), mais vous ne devriez plus l' utiliser:
    • @Before public void initMocks() { MockitoAnnotations.initMocks(this); }
  4. Si vous souhaitez utiliser une maquette dans un seul des tests et que vous ne souhaitez pas l'exposer à d'autres tests de la même classe de test, utilisez
    • X x = mock(X.class)

(1) et (2) et (3) s'excluent mutuellement.
(4) peut être utilisé en combinaison avec les autres.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.