web-dev-qa-db-fra.com

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

Je suis novice en développement et en tests unitaires en particulier. Je suppose que mon exigence est assez simple, mais je tiens à connaître l'opinion d'autres personnes à 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";
    }
}

Supposons que j'écris un test unitaire pour tester la méthode First.doSecond(). Cependant, supposons que je veuille Mock Second.doSecond() class comme tel. J'utilise Mockito pour faire cela.

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

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

Je constate que les moqueries ne prennent pas effet et que l'assertion échoue. N'y a-t-il aucun moyen de se moquer des variables membres d'une classe que je veux tester. ?

119
Anand Hemmige

Vous devez fournir un moyen d'accéder aux variables membres afin de pouvoir passer une simulation (les méthodes les plus courantes sont une méthode setter ou un constructeur qui prend un paramètre).

Si votre code ne permet pas de le faire, il est incorrectement factorisé pour TDD (Test Driven Development).

76
kittylyst

Ce n'est pas possible si vous ne pouvez pas changer votre code. Mais j'aime l'injection de dépendance et Mockito le soutient:

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 gentil et facile.

55
Janning

Si vous examinez attentivement votre code, vous verrez que la propriété second de votre test est toujours une instance de Second, pas une réplique (vous ne transmettez pas la réplique à first dans votre code).

Le moyen le plus simple serait de créer un séparateur pour second dans First class et de le transmettre explicitement.

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());
}

Une autre solution serait de passer une instance de Second en tant que paramètre constructeur de First.

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 le faire, 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 dans lequel vous devez tester une bibliothèque externe car l'auteur ne l'a pas :))

32
soulcheck

Si vous ne pouvez pas modifier la variable membre, l’opération inverse consiste à utiliser powerMockit et à 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.

6
user1509463

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

Tout d'abord, j'ai créé une classe TestUtils qui contient de nombreux utilitaires utiles, notamment ces méthodes de réflexion. L’accès à la réflexion est un peu bizarre à 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 paquet 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 classe que vous ne contrôlez pas non plus.

@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 l'idée générale. N'hésitez pas à récupérer le code et à l'utiliser si vous le trouvez utile.

6
dave

Beaucoup de personnes vous ont déjà conseillé de repenser votre code pour le rendre plus testable - un conseil judicieux et généralement plus simple que ce que je vais vous 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), offrant des fonctionnalités supplémentaires. Cela inclut la possibilité de demander à un constructeur de retourner une maquette. Puissant, mais un peu compliqué - utilisez-le judicieusement.

Vous utilisez un autre coureur factice. Et vous devez préparer la classe qui va appeler le constructeur. (Notez qu'il s'agit d'un gotcha commun - 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 retourne une maquette

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));
1
jwepurchase

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

Whitebox.setInternalState(first, "second", sec);
0
Szymon Zwoliński

Oui, cela peut être fait, comme le montre le test suivant (écrit avec l’API moqueuse 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 manière dont le moquage est implémenté dans Mockito, où une sous-classe de la classe à simuler est créée; le comportement de ce sous-type "mock" peut être simulé, vous devez donc le faire utiliser par le code testé à la place de toute autre instance.

0
Rogério