Est-ce une utilisation appropriée de la méthode de réinitialisation de Mockito?

68

J'ai une méthode privée dans ma classe de test qui construit un Barobjet couramment utilisé . Le Barconstructeur appelle la someMethod()méthode dans mon objet simulé:

private @Mock Foo mockedObject; // My mocked object
...

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
}

Dans certaines de mes méthodes de test, je souhaite vérifier someMethodque ce test particulier a également été invoqué. Quelque chose comme ce qui suit:

@Test
public void someTest() {
  Bar bar = getBar();

  // do some things

  verify(mockedObject).someMethod(); // <--- will fail
}

Cela échoue car l'objet simulé a été someMethodinvoqué deux fois. Je ne veux pas que mes méthodes de test se soucient des effets secondaires de ma getBar()méthode, donc serait-il raisonnable de réinitialiser mon objet fictif à la fin de getBar()?

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
  reset(mockedObject); // <-- is this OK?
}

Je demande, parce que la documentation suggère que réinitialiser des objets fictifs est généralement révélateur de mauvais tests. Cependant, cela me convient.

Alternative

Le choix alternatif semble appeler:

verify(mockedObject, times(2)).someMethod();

ce qui à mon avis oblige chaque test à connaître les attentes de getBar(), sans aucun gain.

Duncan Jones
la source

Réponses:

60

Je crois que c'est l'un des cas où l'utilisation reset()est ok. Le test que vous écrivez teste que "certaines choses" déclenchent un seul appel someMethod(). L'écriture de la verify()déclaration avec un nombre différent d'invocations peut prêter à confusion.

  • atLeastOnce() permet de faux positifs, ce qui est une mauvaise chose car vous voulez que vos tests soient toujours corrects.
  • times(2)empêche le faux positif, mais donne l'impression que vous attendez deux invocations plutôt que de dire "je sais que le constructeur en ajoute un". De plus, si quelque chose change dans le constructeur pour ajouter un appel supplémentaire, le test a maintenant une chance d’être faussement positif. Et supprimer l'appel entraînerait l'échec du test, car celui-ci est maintenant faux, et non ce qui est testé.

En utilisant reset()la méthode d'assistance, vous évitez ces deux problèmes. Cependant, vous devez faire attention à ce que cela ne réinitialise pas non plus les attaques que vous avez effectuées. Soyez donc averti. La principale raison reset()est découragée est d'empêcher

bar = mock(Bar.class);
//do stuff
verify(bar).someMethod();
reset(bar);
//do other stuff
verify(bar).someMethod2();

Ce n’est pas ce que le PO essaie de faire. Je suppose que l’OP a un test qui vérifie l’invocation dans le constructeur. Pour ce test, la réinitialisation permet d’isoler cette action unique et son effet. C'est l'un des rares cas avec qui reset()peut être utile comme. Les autres options qui ne l'utilisent pas ont toutes des inconvénients. Le fait que l'OP ait écrit cet article montre qu'il réfléchit à la situation et n'utilise pas aveuglément la méthode de réinitialisation.

unholysampler
la source
17
Je souhaite que Mockito fournisse un appel resetInteractions () pour simplement oublier les interactions passées dans le but de vérifier (..., times (...)) et de conserver le stubbing. Cela rendrait les situations de test de {setup; acte; vérifier;} que beaucoup plus facile à traiter. Ce serait {setup; resetInteractions; acte; verify}
Arkadiy
2
En fait, depuis Mockito 2.1, il fournit un moyen d'effacer les invocations sans réinitialiser les stubs:Mockito.clearInvocations(T... mocks)
Colin D Bennett
6

Les utilisateurs de Smart Mockito n’utilisent guère la fonction de réinitialisation, car ils savent que cela pourrait être le signe de mauvais tests. Normalement, vous n'avez pas besoin de réinitialiser vos modèles, créez-en de nouveaux pour chaque méthode de test.

Au lieu de reset()penser à écrire des méthodes de test simples, petites et ciblées sur des tests longs et sur-spécifiés. La première odeur de code potentielle est reset()au milieu de la méthode de test.

Extrait des documents mockito .

Mon conseil est que vous essayez d'éviter d'utiliser reset(). À mon avis, si vous appelez deux fois à une méthode, cela doit être testé (il s’agit peut-être d’un accès à une base de données ou d’un autre processus long dont vous souhaitez prendre soin).

Si cela vous est égal, vous pouvez utiliser:

verify(mockedObject, atLeastOnce()).someMethod();

Notez que ce dernier risque de générer un résultat faux, si vous appelez someMethod depuis getBar, et non après (il s’agit d’un comportement incorrect, mais le test n’échouera pas).

greuze
la source
2
Oui, j'ai vu cette citation exacte (je l'ai liée à partir de ma question). Actuellement, je ne vois pas encore d'argument raisonnable sur la raison pour laquelle mon exemple ci-dessus est "mauvais". Pouvez-vous en fournir un?
Duncan Jones
Si vous devez réinitialiser vos objets fictifs, il semble que vous essayez de tester trop de choses dans votre test. Vous pouvez le diviser en deux tests, en testant des choses plus petites. Dans tous les cas, j'ignore pourquoi vous vérifiez dans la méthode getBar qu'il est difficile de suivre ce que vous testez. Je vous recommande de concevoir votre test en pensant à ce que votre classe devrait faire (si vous devez appeler certaines méthodes exactement deux fois, au moins une fois, une seule fois, jamais, etc.) et effectuer toutes les vérifications au même endroit.
Greuze
J'ai modifié ma question pour souligner que le problème persiste même si je n'appelle pas verifyma méthode privée (ce qui, j'en conviens, n'appartient probablement pas à cette méthode). Je me réjouis de vos commentaires pour savoir si votre réponse changerait.
Duncan Jones
Il y a beaucoup de bonnes raisons d'utiliser reset, je ne ferais pas trop attention à cette citation de mockito. Le gestionnaire de classe JUnit de Spring peut, lors de l'exécution d'une suite de tests, provoquer des interactions indésirables, notamment si vous effectuez des tests impliquant des appels de base de données fictifs ou des appels impliquant des méthodes privées sur lesquelles vous ne souhaitez pas utiliser la réflexion.
Sandy Simonton
Je trouve généralement cela difficile lorsque je veux tester plusieurs choses, mais JUnit n’offre tout simplement pas de manière agréable (!) De paramétrer des tests. Contrairement à NUnit, par exemple avec des annotations.
Stefan Hendriks
3

Absolument pas. Comme c'est souvent le cas, la difficulté que vous rencontrez lors de la rédaction d'un test vierge est un signal d'alarme majeur pour la conception de votre code de production. Dans ce cas, la meilleure solution consiste à refactoriser votre code afin que le constructeur de Bar n'appelle aucune méthode.

Les constructeurs doivent construire et non exécuter la logique. Prenez la valeur de retour de la méthode et transmettez-la en tant que paramètre constructeur.

new Bar(mockedObject);

devient:

new Bar(mockedObject.someMethod());

Si cela devait entraîner la duplication de cette logique à plusieurs endroits, envisagez de créer une méthode fabrique pouvant être testée indépendamment de votre objet Bar:

public Bar createBar(MockedObject mockedObject) {
    Object dependency = mockedObject.someMethod();
    // ...more logic that used to be in Bar constructor
    return new Bar(dependency);
}

Si cette refactorisation est trop difficile, utiliser reset () est un bon moyen de contourner le problème. Mais soyons clairs: cela indique que votre code est mal conçu.

tonicsoft
la source