Verspotten von Mitgliedsvariablen einer Klasse mit Mockito

136

Ich bin ein Neuling in der Entwicklung und insbesondere bei Unit-Tests. Ich denke, meine Anforderung ist ziemlich einfach, aber ich bin gespannt darauf, andere Gedanken dazu zu kennen.

Angenommen, ich habe zwei Klassen wie diese -

public class First {

    Second second ;

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

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

class Second {

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

Angenommen, ich schreibe Unit-Test-to-Test- First.doSecond()Methode. Angenommen, ich möchte die Second.doSecond()Klasse so verspotten . Ich benutze Mockito, um dies zu tun.

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

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

Ich sehe, dass die Verspottung nicht wirksam wird und die Behauptung fehlschlägt. Gibt es keine Möglichkeit, die Mitgliedsvariablen einer Klasse zu verspotten, die ich testen möchte? ?

Anand Hemmige
quelle

Antworten:

86

Sie müssen eine Möglichkeit für den Zugriff auf die Elementvariablen bereitstellen, damit Sie einen Mock übergeben können (die gebräuchlichste Methode ist eine Setter-Methode oder ein Konstruktor, der einen Parameter verwendet).

Wenn Ihr Code dies nicht bietet, wird er für TDD (Test Driven Development) falsch berücksichtigt.

kittylyst
quelle
4
Vielen Dank. Ich sehe es. Ich frage mich nur, wie ich dann Integrationstests mit mock durchführen kann, bei denen es möglicherweise viele interne Methoden gibt, Klassen, die möglicherweise verspottet werden müssen, aber nicht unbedingt verfügbar sind, um vorher über setXXX () festgelegt zu werden.
Anand Hemmige
2
Verwenden Sie ein Abhängigkeitsinjektionsframework mit einer Testkonfiguration. Erstellen Sie ein Sequenzdiagramm des Integrationstests, den Sie durchführen möchten. Berücksichtigen Sie das Sequenzdiagramm in den Objekten, die Sie tatsächlich steuern können. Dies bedeutet, dass Sie, wenn Sie mit einer Framework-Klasse arbeiten, die das oben gezeigte abhängige Objekt-Anti-Pattern aufweist, das Objekt und sein schlecht faktorisiertes Element als eine Einheit im Sinne des Sequenzdiagramms betrachten sollten. Seien Sie bereit, das Factoring von Code, den Sie steuern, anzupassen, um ihn testbarer zu machen.
Kittylyst
9
Lieber @kittylyst, ja, wahrscheinlich ist es aus TDD-Sicht oder aus rationaler Sicht falsch. Aber manchmal arbeitet ein Entwickler an Orten, an denen überhaupt nichts Sinn macht, und das einzige Ziel, das man hat, ist nur, die von Ihnen zugewiesenen Geschichten zu vervollständigen und wegzugehen. Ja, es ist falsch, es macht keinen Sinn, unqualifizierte Leute übernehmen die Schlüsselentscheidungen und all diese Dinge. Letztendlich gewinnen Anti-Patterns also viel.
Amanas
1
Ich bin neugierig, wenn ein Klassenmitglied keinen Grund hat, von außen eingestellt zu werden, warum sollten wir dann einen Setter erstellen, nur um ihn zu testen? Stellen Sie sich vor, die 'zweite' Klasse hier ist tatsächlich ein FileSystem-Manager oder -Tool, das während der Erstellung des zu testenden Objekts initialisiert wurde. Ich habe alle Gründe, diesen FileSystem-Manager zu verspotten, um die erste Klasse zu testen, und keinen Grund, sie zugänglich zu machen. Ich kann das in Python machen, warum also nicht mit Mockito?
Zangdar
65

Dies ist nicht möglich, wenn Sie Ihren Code nicht ändern können. Aber ich mag Abhängigkeitsinjektion und Mockito unterstützt sie:

public class First {    
    @Resource
    Second second;

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

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

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

Das ist sehr schön und einfach.

Janning
quelle
2
Ich denke, dies ist eine bessere Antwort als die anderen, weil InjectMocks.
Sudocoder
Es ist lustig, wie man als Testneuling wie ich bestimmten Bibliotheken und Frameworks vertraut. Ich war der Annahme , dies war nur eine schlechte Idee einen Bedarf anzeigt , neu zu gestalten ... bis du mir gezeigt , es ist in der Tat möglich (sehr klar und sauber) in Mockito.
Mike Nagetier
9
Was ist @Resource ?
IgorGanapolsky
3
@IgorGanapolsky the @ Resource ist eine Anmerkung, die vom Java Spring-Framework erstellt / verwendet wird. Dies ist eine Möglichkeit, Spring anzuzeigen, dass es sich um eine von Spring verwaltete Bean / ein Objekt handelt. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire Es ist keine Mockito-Sache, aber da es in der nicht testenden Klasse verwendet wird, muss es in der verspottet werden Prüfung.
Grez.Kev
Ich verstehe diese Antwort nicht. Sie sagen, es ist nicht möglich, dann zeigen Sie, dass es möglich ist? Was genau ist hier nicht möglich?
Goldname
35

Wenn Sie sich Ihren Code genau ansehen, werden Sie feststellen, dass die secondEigenschaft in Ihrem Test immer noch eine Instanz von Secondund kein Mock ist (Sie geben den Mock firstin Ihrem Code nicht weiter).

Der einfachste Weg wäre, einen Setter für secondin der FirstKlasse zu erstellen und ihm den Mock explizit zu übergeben.

So was:

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

Eine andere Möglichkeit wäre, eine SecondInstanz als FirstKonstruktorparameter zu übergeben.

Wenn Sie den Code nicht ändern können, besteht meiner Meinung nach die einzige Möglichkeit darin, Reflection zu verwenden:

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

Aber Sie können es wahrscheinlich, da es selten ist, Tests mit Code durchzuführen, den Sie nicht kontrollieren (obwohl man sich ein Szenario vorstellen kann, in dem Sie eine externe Bibliothek testen müssen, weil der Autor dies nicht getan hat :)).

Soulcheck
quelle
Verstanden. Ich werde wahrscheinlich mit Ihrem ersten Vorschlag gehen.
Anand Hemmige
Nur neugierig, gibt es eine Möglichkeit oder API, die Sie kennen und die ein Objekt / eine Methode auf Anwendungs- oder Paketebene verspotten kann. ? Ich denke, was ich sage, ist, dass es im obigen Beispiel, wenn ich das 'zweite' Objekt verspotte, eine Möglichkeit gibt, jede Instanz von Second zu überschreiben, die während des gesamten Testlebenszyklus verwendet wird. ?
Anand Hemmige
@AnandHemmige Tatsächlich ist der zweite (Konstruktor) sauberer, da unnötige "zweite" Instanzen vermieden werden. Ihre Klassen werden auf diese Weise schön entkoppelt.
Soulcheck
10
Mockito bietet einige nette Anmerkungen, mit denen Sie Ihre Mocks in private Variablen einfügen können. Kommentieren Sie Second mit @Mockund Annotieren Sie First mit @InjectMocksund instanziieren Sie First im Initialisierer. Mockito wird automatisch das Beste tun, um einen Ort zu finden, an dem das zweite Modell in die erste Instanz eingefügt werden kann, einschließlich des Festlegens privater Felder, die dem Typ entsprechen.
Jhericks
@Mockwar um in 1.5 (vielleicht früher, ich bin nicht sicher). 1.8.3 eingeführt @InjectMockssowie @Spyund @Captor.
Jhericks
7

Wenn Sie die Mitgliedsvariable nicht ändern können, können Sie PowerMockit verwenden und aufrufen

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

Das Problem ist nun, dass JEDER Aufruf von new Second dieselbe verspottete Instanz zurückgibt. Aber in Ihrem einfachen Fall wird dies funktionieren.

user1509463
quelle
6

Ich hatte das gleiche Problem, bei dem kein privater Wert festgelegt wurde, weil Mockito keine Superkonstruktoren aufruft. Hier ist, wie ich das Verspotten mit Nachdenken verstärke.

Zuerst habe ich eine TestUtils-Klasse erstellt, die viele hilfreiche Dienstprogramme enthält, einschließlich dieser Reflexionsmethoden. Der Reflexionszugriff ist jedes Mal etwas schwierig zu implementieren. Ich habe diese Methoden erstellt, um Code für Projekte zu testen, die aus dem einen oder anderen Grund kein Verspottungspaket hatten, und ich wurde nicht eingeladen, ihn einzuschließen.

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

}

Dann kann ich die Klasse mit einer privaten Variablen wie dieser testen. Dies ist nützlich, um sich tief in Klassenbäumen zu verspotten, auf die Sie ebenfalls keine Kontrolle haben.

@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
}

Ich habe meinen Code aus meinem eigentlichen Projekt hier auf Seite geändert. Möglicherweise liegt ein oder zwei Kompilierungsprobleme vor. Ich denke, Sie haben eine allgemeine Vorstellung. Nehmen Sie den Code und verwenden Sie ihn, wenn Sie ihn nützlich finden.

Dave
quelle
Könnten Sie Ihren Code mit einem tatsächlichen Anwendungsfall erklären? wie die öffentliche Klasse tobeMocker () {private ClassObject classObject; } Wobei classObject dem zu ersetzenden Objekt entspricht.
Jasper Lankhorst
In Ihrem Beispiel, wenn ToBeMocker-Instanz = new ToBeMocker (); und ClassObject someNewInstance = new ClassObject () {@Override // so etwas wie eine externe Abhängigkeit}; dann TestUtils.refelctSetValue (Instanz "classObject", someNewInstance); Beachten Sie, dass Sie herausfinden müssen, was Sie zum Verspotten überschreiben möchten. Nehmen wir an, Sie haben eine Datenbank und diese Überschreibung gibt einen Wert zurück, den Sie nicht auswählen müssen. Zuletzt hatte ich einen Servicebus, mit dem ich die Nachricht nicht verarbeiten, sondern sicherstellen wollte, dass sie empfangen wurde. Daher habe ich die private Businstanz so eingestellt: Hilfreich?
Dave
Sie müssen sich vorstellen, dass dieser Kommentar formatiert wurde. Es wurde entfernt. Dies funktioniert auch nicht mit Java 9, da der private Zugriff gesperrt wird. Sobald wir eine offizielle Version haben, müssen wir mit einigen anderen Konstrukten arbeiten und können mit ihren tatsächlichen Grenzen arbeiten.
Dave
1

Viele andere haben Ihnen bereits geraten, Ihren Code zu überdenken, um ihn testbarer zu machen - gute Ratschläge und normalerweise einfacher als das, was ich vorschlagen werde.

Wenn Sie den Code nicht ändern können, um ihn testbarer zu machen, wählen Sie PowerMock: https://code.google.com/p/powermock/

PowerMock erweitert Mockito (damit Sie kein neues Mock-Framework erlernen müssen) und bietet zusätzliche Funktionen. Dies beinhaltet die Möglichkeit, dass ein Konstruktor einen Mock zurückgibt. Kraftvoll, aber etwas kompliziert - verwenden Sie es also mit Bedacht.

Sie verwenden einen anderen Mock-Läufer. Und Sie müssen die Klasse vorbereiten, die den Konstruktor aufruft. (Beachten Sie, dass dies ein häufiges Problem ist. Bereiten Sie die Klasse vor, die den Konstruktor aufruft, nicht die konstruierte Klasse.)

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

Anschließend können Sie in Ihrem Testaufbau die whenNew-Methode verwenden, damit der Konstruktor einen Mock zurückgibt

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

Ja, dies kann durchgeführt werden, wie der folgende Test zeigt (geschrieben mit der von mir entwickelten JMockit-Mocking-API):

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

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

Mit Mockito kann ein solcher Test jedoch nicht geschrieben werden. Dies liegt an der Art und Weise, wie das Verspotten in Mockito implementiert wird, wo eine Unterklasse der zu verspottenden Klasse erstellt wird. Nur Instanzen dieser "Schein" -Unterklasse können ein verspottetes Verhalten aufweisen. Daher muss der getestete Code sie anstelle einer anderen Instanz verwenden.

Rogério
quelle
3
Die Frage war nicht, ob JMockit besser ist als Mockito, sondern wie man es in Mockito macht. Bleiben Sie beim Aufbau eines besseren Produkts, anstatt nach einer Möglichkeit zu suchen, die Konkurrenz zu vernichten!
TheZuck
8
Das Originalplakat sagt nur, dass er Mockito benutzt; Es ist nur impliziert, dass Mockito eine feste und harte Anforderung ist, so dass der Hinweis, dass JMockit mit dieser Situation umgehen kann, nicht so unangemessen ist.
Bombe
0

Wenn Sie eine Alternative zu ReflectionTestUtils von Spring in mockito wünschen, verwenden Sie

Whitebox.setInternalState(first, "second", sec);
Szymon Zwoliński
quelle
Willkommen bei Stack Overflow! Es gibt andere Antworten, die die Frage des OP stellen, und sie wurden vor vielen Jahren veröffentlicht. Stellen Sie beim Posten einer Antwort sicher, dass Sie entweder eine neue Lösung oder eine wesentlich bessere Erklärung hinzufügen, insbesondere wenn Sie ältere Fragen beantworten oder andere Antworten kommentieren.
help-info.de