web-dev-qa-db-fra.com

Est-ce vraiment une mauvaise pratique de se moquer d'un POJO (objet de valeur) si vous ne vous souciez pas de ce qu'il contient?

Dans les conseils de Mockito sur la façon d'écrire de bons tests, il est écrit que nous ne devons pas nous moquer des objets de valeur ( https://github.com/mockito/mockito/wiki/How-to-write-good-tests # dont-mock-value-objects ). Ils disent même

Parce que l'instanciation de l'objet est trop douloureuse!? => pas une raison valable.

Mais je ne comprends pas. Et si nous ne nous soucions pas vraiment de ce que contient cet objet? Disons simplement que j'ai une classe d'objets de valeur "Foo" qui n'a pas de constructeurs simples (elle a un constructeur all-args et contient des objets qui ont également besoin de constructeurs all-args). Disons également que j'ai le service suivant:

public class MyService {
    private SomeService someService;
    private OtherService otherService;

    public void myAwesomeMethod() {
        Foo foo = someService.getFoo();
        someOtherService.doStuff(foo);
    }
}

Et je dois tester cette méthode. Clairement ici, je ne me soucie pas de ce que contient foo parce que je le passe simplement à une autre méthode. Si je veux le tester, dois-je vraiment créer un objet foo avec des constructeurs/constructeurs imbriqués? Pour moi, il serait vraiment facile de se moquer de foo même s'il s'agit d'un POJO:

public class MyServiceTest {
    @InjectMocks
    MyService myService;

    @Mock
    private SomeService someService;

    @Mock
    private OtherService otherService;

    @Test
    public void testMyAwesomeMethod() {
        Foo mockedFoo = Mockito.mock(Foo.class);

        Mockito.when(someService.getFoo()).thenReturn(mockedFoo);

        Mockito.verify(otherService).doStuff(mockedFoo);
    }

}

Mais cela ne respecterait pas les directives données par Mockito. Y a-t-il une autre alternative?

6
Ricola

Les talons (que vous avez appelés simulateurs dans votre question) existent pour une raison: ils permettent de remplacer le code métier des classes dont vous dépendez dans la méthode que vous testez par des instructions très basiques qui fournissent suffisamment de données pour les tests méthode. Les simulations, tout comme les stubs, remplacent la logique métier par une logique personnalisée requise pour les tests.

  • Si un POJO n'a pas de code d'entreprise (c'est-à-dire s'il s'agit également d'un objet de données), les talons et les maquettes seraient inutiles. L'utilisation de tels objets de données dans les tests devrait être assez simple. Si la méthode a besoin d'un objet de données en entrée, créez-en simplement un dans le test.

    Certains objets de données peuvent être très volumineux et contenir des dizaines de propriétés. Dans certaines langues, vous pouvez initialiser partiellement un objet de données en définissant uniquement les propriétés dont vous avez besoin pendant le test; dans d'autres langues, vous devez l'initialiser complètement. Si tel est votre cas, la création d'une méthode statique simple qui crée l'objet de données avec les valeurs par défaut et la réutilisation de cette méthode dans tous les tests pourrait être une solution.

  • Si un POJO contient un code métier, vous devez en isoler vos tests unitaires, comme vous le feriez pour n'importe quelle autre classe. Ici, les talons et les maquettes sont très bien, et cela ne fait aucune différence si l'objet moqué est un POJO ou autre chose.

9
Arseni Mourzenko

La meilleure ligne de votre lien à mon avis est

Ne vous moquez pas de tout, c'est un anti-modèle

Si tout est moqué, testons-nous vraiment le code de production? N'hésitez pas à ne pas vous moquer!

Habituellement, vous voulez tester votre code avec des entrées réelles et des sorties attendues et généralement ce seront des structures de données ou des pojos/pocos

c'est à dire.

actual = myfunc(input)
Assert.AreEqual(actual, expected)

Il n'est pas logique de se moquer de cette entrée ou sortie car elle est configurée spécifiquement pour ce test. Vous vous en souciez toujours.

Dans votre exemple, vous n'avez pas d'entrée, de sortie ni de logique à effectuer. Même si vous avez un exemple réel où le code passe simplement un objet d'un service à un autre, vous voulez sûrement tester des cas Edge comme des valeurs nulles ou un très grand objet? Vous devriez vous soucier de ce qu'est cet objet.

3
Ewan

Vous pourriez rencontrer une odeur de code.

Clairement ici, je ne me soucie pas de ce que contient foo parce que je le passe simplement à une autre méthode.

Ce qui me ressort dans cet exemple, c'est que votre code ne se soucie même pas que l'objet échangé soit un Foo.

public class MyService<T> {
    private Java.util.function.Supplier<? extends T> someService;
    private Java.util.function.Consumer<? super T> otherService;

    public void myAwesomeMethod() {
        T foo = someService.get();
        someOtherService.accept(foo);
    }
}

Ensuite, dans votre test unitaire, au lieu d'essayer d'utiliser un Foo, ou de créer un double de test que vous pouvez utiliser à la place d'un Foo, vous pouvez injecter une paire fournisseur/consommateur.

Disons simplement que j'ai une classe d'objets de valeur "Foo" qui n'a pas de constructeurs simples (elle a un constructeur all-args et contient des objets qui ont également besoin de constructeurs all-args).

Un autre modèle courant consiste à utiliser testeurs de données pour créer les valeurs dont vous avez besoin. Les créateurs de données de test utilisent généralement interfaces fluides pour communiquer dans un test les détails de l'objet qui sont important dans le contexte du test .

@Test
public void testMyAwesomeMethod() {
    Foo mockedFoo = new FooBuilder().anyOldFoo();

    Mockito.when(someService.getFoo()).thenReturn(mockedFoo);

    Mockito.verify(otherService).doStuff(mockedFoo);
}

Ici, l'interaction avec le générateur indique que le comportement de ce test ne devrait pas dépendre de la valeur foo spécifique.

Si vous plissez les yeux, vous pourriez voir que le test affirme une propriété

public void testMyAwesomeMethod() {
    for (Foo candidate : allPossibleFoo()) {
        Mockito.when(someService.getFoo()).thenReturn(candidate);
        // ...
    }
}

Aucune de ces approches de constructeur ne résout directement le problème d'invocation du constructeur fou; le constructeur ne fait que déplacer cette complexité hors du test .

2
VoiceOfUnreason

Vous avez quelque peu raison.

Lorsque vous vous moquez d'un objet, vous supprimez généralement les méthodes pour renvoyer des valeurs prédéfinies à la place. Mais il n'y a pas vraiment de bonne raison de le faire pour un objet de valeur, utilisez simplement l'objet de valeur à la place.

Cependant, dans ce scénario particulier, vous ne supprimez aucune des méthodes. Vous êtes en train de vérifier que le même objet renvoyé par une fonction est passé à une autre. Vous n'avez donc pas les inconvénients de vous moquer de l'objet valeur.

Néanmoins, vous devriez presque certainement avoir un code de test qui construit un Foo. Le test pour SomeService devrait probablement assertEquals le résultat de getFoo avec un peu de Foo construit. Le test de OtherService doit passer un objet Foo construit à doStuff. Vous devez avoir un code quelque part qui construit des objets Foo pour ces tests, vous devriez donc être en mesure de refactoriser facilement ce code de construction dans un endroit où il peut être réutilisé.

Cependant, en général, il est suspect que MyService ne veuille appeler aucune méthode sur Foo. C'est souvent un signe que MyService ne devrait pas du tout traiter avec Foo. Si tout MyService prend-il un objet de SomeService et le passe-t-il à OtherService quel est l'intérêt de MyService? Simplifiez votre code et connectez directement les deux services.

1
Winston Ewert