Mocking des variables membres d'une classe à l'aide de Mockito

136

Je suis un novice en développement et en tests unitaires en particulier. Je suppose que mon exigence est assez simple, mais je souhaite connaître l'opinion des autres à ce sujet.

Supposons que j'ai deux classes comme ça -

public class First {

    Second second ;

    public First(){
        second = new Second();
    }

    public String doSecond(){
        return second.doSecond();
    }
}

class Second {

    public String doSecond(){
        return "Do Something";
    }
}

Disons que j'écris un test unitaire pour tester First.doSecond() méthode de . Cependant, supposons que je veuille moquer la Second.doSecond()classe comme ça. J'utilise Mockito pour ce faire.

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Je vois que la moquerie n'a pas d'effet et que l'assertion échoue. N'existe-t-il aucun moyen de se moquer des variables membres d'une classe que je souhaite tester. ?

Anand Hemmige
la source

Réponses:

86

Vous devez fournir un moyen d'accéder aux variables membres afin de pouvoir passer dans un simulacre (les moyens les plus courants seraient une méthode setter ou un constructeur qui prend un paramètre).

Si votre code ne fournit pas un moyen de le faire, il est incorrectement pris en compte pour TDD (Test Driven Development).

kittylyst
la source
4
Merci. Je le vois. Je me demande simplement comment puis-je effectuer des tests d'intégration en utilisant des simulations où il peut y avoir de nombreuses méthodes internes, des classes qui peuvent avoir besoin d'être simulées, mais pas nécessairement disponibles pour être définies via un setXXX () avant la main.
Anand Hemmige
2
Utilisez un framework d'injection de dépendances, avec une configuration de test. Dessinez un diagramme de séquence du test d'intégration que vous essayez de faire. Factorisez le diagramme de séquence dans les objets que vous pouvez réellement contrôler. Cela signifie que si vous travaillez avec une classe de structure qui a l'anti-modèle d'objet dépendant que vous montrez ci-dessus, vous devez considérer l'objet et son membre mal pondéré comme une seule unité en termes de diagramme de séquence. Soyez prêt à ajuster la factorisation de tout code que vous contrôlez, pour le rendre plus testable.
kittylyst
9
Cher @kittylyst, oui c'est probablement faux du point de vue TDD ou de tout type de point de vue rationnel. Mais parfois, un développeur travaille dans des endroits où rien n'a de sens et le seul objectif que l'on a est juste de terminer les histoires que vous avez assignées et de s'en aller. Oui, c'est faux, cela n'a aucun sens, les personnes non qualifiées prennent les décisions clés et tout le reste. Donc, à la fin de la journée, les anti-modèles gagnent beaucoup.
amanas
1
Je suis curieux de savoir si un membre de la classe n'a aucune raison d'être défini de l'extérieur, pourquoi devrions-nous créer un setter uniquement dans le but de le tester? Imaginez que la classe 'Second' ici soit en fait un gestionnaire ou un outil FileSystem, initialisé lors de la construction de l'objet à tester. J'ai toutes les raisons de vouloir me moquer de ce gestionnaire FileSystem, afin de tester la classe First, et aucune raison de le rendre accessible. Je peux faire cela en Python, alors pourquoi pas avec Mockito?
Zangdar le
65

Ce n'est pas possible si vous ne pouvez pas modifier votre code. Mais j'aime l'injection de dépendances et Mockito le prend en charge:

public class First {    
    @Resource
    Second second;

    public First() {
        second = new Second();
    }

    public String doSecond() {
        return second.doSecond();
    }
}

Votre test:

@RunWith(MockitoJUnitRunner.class)
public class YourTest {
   @Mock
   Second second;

   @InjectMocks
   First first = new First();

   public void testFirst(){
      when(second.doSecond()).thenReturn("Stubbed Second");
      assertEquals("Stubbed Second", first.doSecond());
   }
}

C'est très agréable et facile.

Janning
la source
2
Je pense que c'est une meilleure réponse que les autres parce que InjectMocks.
sudocoder
C'est drôle comme on arrive, en tant que débutant en test comme moi, à faire confiance à certaines bibliothèques et frameworks. Je supposais que c'était une mauvaise idée indiquant la nécessité de revoir ... jusqu'à ce que vous me l'a montré est en effet possible (très clairement et proprement) dans Mockito.
mike rodent
9
Qu'est-ce que @Resource ?
IgorGanapolsky
3
@IgorGanapolsky the @ Resource est une annotation créée / utilisée par le framework Java Spring. C'est une façon d'indiquer à Spring qu'il s'agit d'un bean / objet géré par Spring. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire Ce n'est pas une chose mockito, mais parce qu'il est utilisé dans la classe non testing, il doit être moqué dans le tester.
Grez.Kev
Je ne comprends pas cette réponse. Vous dites que ce n'est pas possible alors vous montrez que c'est possible? Qu'est-ce qui n'est pas possible exactement ici?
Goldname
35

Si vous regardez attentivement votre code, vous verrez que la secondpropriété de votre test est toujours une instance de Second, pas une simulation (vous ne transmettez pas la simulation firstdans votre code).

Le moyen le plus simple serait de créer un setter pour seconden Firstclasse et de lui passer explicitement le simulacre.

Comme ça:

public class First {

Second second ;

public First(){
    second = new Second();
}

public String doSecond(){
    return second.doSecond();
}

    public void setSecond(Second second) {
    this.second = second;
    }


}

class Second {

public String doSecond(){
    return "Do Something";
}
}

....

public void testFirst(){
Second sec = mock(Second.class);
when(sec.doSecond()).thenReturn("Stubbed Second");


First first = new First();
first.setSecond(sec)
assertEquals("Stubbed Second", first.doSecond());
}

Un autre serait de passer une Secondinstance en tant que Firstparamètre de constructeur de.

Si vous ne pouvez pas modifier le code, je pense que la seule option serait d'utiliser la réflexion:

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");


    First first = new First();
    Field privateField = PrivateObject.class.
        getDeclaredField("second");

    privateField.setAccessible(true);

    privateField.set(first, sec);

    assertEquals("Stubbed Second", first.doSecond());
}

Mais vous pouvez probablement, car il est rare de faire des tests sur du code que vous ne contrôlez pas (bien que l'on puisse imaginer un scénario où vous devez tester une bibliothèque externe car son auteur ne l'a pas fait :))

soulcheck
la source
Je l'ai. Je vais probablement accepter votre première suggestion.
Anand Hemmige
Juste curieux, y a-t-il un moyen ou une API dont vous avez connaissance qui peut se moquer d'un objet / d'une méthode au niveau de l'application ou du package. ? Je suppose que ce que je veux dire, c'est que dans l'exemple ci-dessus, lorsque je me moque de l'objet «Second», y a-t-il un moyen de remplacer chaque instance de Second qui est utilisée tout au long du cycle de vie des tests. ?
Anand Hemmige
@AnandHemmige en fait le second (constructeur) est plus propre, car il évite de créer des instances de `Second 'inutiles. Vos cours sont bien découplés de cette façon.
soulcheck
10
Mockito fournit de jolies annotations pour vous permettre d'injecter vos simulacres dans des variables privées. Annoter Second avec @Mocket annoter First avec @InjectMockset instancier First dans l'initialiseur. Mockito fera automatiquement de son mieux pour trouver un endroit pour injecter la deuxième maquette dans la première instance, y compris en définissant des champs privés qui correspondent au type.
jhericks
@Mockétait autour de 1,5 (peut-être plus tôt, je ne suis pas sûr). 1.8.3 introduit @InjectMocksainsi que @Spyet @Captor.
jhericks
7

Si vous ne pouvez pas modifier la variable membre, l'inverse est d'utiliser powerMockit et d'appeler

Second second = mock(Second.class)
when(second.doSecond()).thenReturn("Stubbed Second");
whenNew(Second.class).withAnyArguments.thenReturn(second);

Maintenant, le problème est que TOUT appel à new Second renverra la même instance simulée. Mais dans votre cas simple, cela fonctionnera.

user1509463
la source
6

J'ai eu le même problème où une valeur privée n'était pas définie car Mockito n'appelle pas les super constructeurs. Voici comment j'augmente la moquerie par la réflexion.

Tout d'abord, j'ai créé une classe TestUtils qui contient de nombreux outils utiles, y compris ces méthodes de réflexion. L'accès à la réflexion est un peu bancal à mettre en œuvre à chaque fois. J'ai créé ces méthodes pour tester du code sur des projets qui, pour une raison ou une autre, n'avaient pas de package moqueur et je n'ai pas été invité à l'inclure.

public class TestUtils {
    // get a static class value
    public static Object reflectValue(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(classToReflect, fieldNameValueToFetch);
            reflectField.setAccessible(true);
            Object reflectValue = reflectField.get(classToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // get an instance value
    public static Object reflectValue(Object objToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameValueToFetch);
            Object reflectValue = reflectField.get(objToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // find a field in the class tree
    public static Field reflectField(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField = null;
            Class<?> classForReflect = classToReflect;
            do {
                try {
                    reflectField = classForReflect.getDeclaredField(fieldNameValueToFetch);
                } catch (NoSuchFieldException e) {
                    classForReflect = classForReflect.getSuperclass();
                }
            } while (reflectField==null || classForReflect==null);
            reflectField.setAccessible(true);
            return reflectField;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch +" from "+ classToReflect);
        }
        return null;
    }
    // set a value with no setter
    public static void refectSetValue(Object objToReflect, String fieldNameToSet, Object valueToSet) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameToSet);
            reflectField.set(objToReflect, valueToSet);
        } catch (Exception e) {
            fail("Failed to reflectively set "+ fieldNameToSet +"="+ valueToSet);
        }
    }

}

Ensuite, je peux tester la classe avec une variable privée comme celle-ci. Ceci est utile pour se moquer des arbres de classes que vous n'avez pas de contrôle également.

@Test
public void testWithRectiveMock() throws Exception {
    // mock the base class using Mockito
    ClassToMock mock = Mockito.mock(ClassToMock.class);
    TestUtils.refectSetValue(mock, "privateVariable", "newValue");
    // and this does not prevent normal mocking
    Mockito.when(mock.somthingElse()).thenReturn("anotherThing");
    // ... then do your asserts
}

J'ai modifié mon code de mon projet actuel ici, en page. Il pourrait y avoir un problème de compilation ou deux. Je pense que vous avez une idée générale. N'hésitez pas à saisir le code et à l'utiliser si vous le trouvez utile.

Dave
la source
Pouvez-vous expliquer votre code avec un cas d'utilisation réel? comme Public class tobeMocker () {Private ClassObject classObject; } Où classObject est égal à l'objet à remplacer.
Jasper Lankhorst
Dans votre exemple, si ToBeMocker instance = new ToBeMocker (); et ClassObject someNewInstance = new ClassObject () {@Override // quelque chose comme une dépendance externe}; then TestUtils.refelctSetValue (instance, "classObject", someNewInstance); Notez que vous devez déterminer ce que vous voulez remplacer pour vous moquer. Disons que vous avez une base de données et ce remplacement renverra une valeur afin que vous n'ayez pas besoin de sélectionner. Plus récemment, j'avais un bus de service dont je ne voulais pas réellement traiter le message, mais je voulais m'assurer qu'il le recevait. Ainsi, j'ai défini l'instance de bus privé de cette façon-utile?
dave
Vous devrez imaginer qu'il y avait un formatage dans ce commentaire. Il a été supprimé. De plus, cela ne fonctionnera pas avec Java 9 car cela bloquera l'accès privé. Nous devrons travailler avec d'autres constructions une fois que nous aurons une version officielle et que nous pourrons travailler avec ses limites réelles.
dave
1

Beaucoup d'autres vous ont déjà conseillé de repenser votre code pour le rendre plus testable - de bons conseils et généralement plus simples que ce que je suis sur le point de suggérer.

Si vous ne pouvez pas modifier le code pour le rendre plus testable, PowerMock: https://code.google.com/p/powermock/

PowerMock étend Mockito (vous n'avez donc pas besoin d'apprendre un nouveau framework fictif), fournissant des fonctionnalités supplémentaires. Cela inclut la possibilité qu'un constructeur renvoie une maquette. Puissant, mais un peu compliqué - alors utilisez-le judicieusement.

Vous utilisez un autre coureur Mock. Et vous devez préparer la classe qui va appeler le constructeur. (Notez qu'il s'agit d'un piège courant - préparez la classe qui appelle le constructeur, pas la classe construite)

@RunWith(PowerMockRunner.class)
@PrepareForTest({First.class})

Ensuite, dans votre configuration de test, vous pouvez utiliser la méthode whenNew pour que le constructeur renvoie un simulacre

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));
jwepurchase
la source
0

Oui, cela peut être fait, comme le montre le test suivant (écrit avec l'API de simulation JMockit, que je développe):

@Test
public void testFirst(@Mocked final Second sec) {
    new NonStrictExpectations() {{ sec.doSecond(); result = "Stubbed Second"; }};

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Avec Mockito, cependant, un tel test ne peut pas être écrit. Cela est dû à la façon dont la moquerie est implémentée dans Mockito, où une sous-classe de la classe à moquer est créée; seules les instances de cette sous-classe «fictive» peuvent avoir un comportement simulé, vous devez donc faire en sorte que le code testé les utilise au lieu de toute autre instance.

Rogério
la source
3
la question n'était pas de savoir si JMockit est meilleur que Mockito, mais plutôt comment le faire dans Mockito. Tenez-vous-en à créer un meilleur produit au lieu de chercher une opportunité de détruire la concurrence!
TheZuck
8
L'affiche originale dit seulement qu'il utilise Mockito; il est seulement implicite que Mockito est une exigence fixe et stricte de sorte que l'indication que JMockit peut gérer cette situation n'est pas si inappropriée.
Bombe
0

Si vous voulez une alternative à ReflectionTestUtils de Spring dans mockito, utilisez

Whitebox.setInternalState(first, "second", sec);
Szymon Zwoliński
la source
Bienvenue dans Stack Overflow! Il y a d'autres réponses qui répondent à la question du PO, et elles ont été publiées il y a de nombreuses années. Lorsque vous publiez une réponse, assurez-vous d'ajouter une nouvelle solution ou une explication nettement meilleure, en particulier lorsque vous répondez à des questions plus anciennes ou commentez d'autres réponses.
help-info.de