Mockito, JUnit und Spring

77

Ich habe erst heute angefangen, etwas über Mockito zu lernen. Ich habe einen einfachen Test geschrieben (mit JUnit, siehe unten), aber ich kann nicht herausfinden, wie ich Scheinobjekte in Spring's verwalteten Beans verwenden kann. Was sind Best Practices für die Arbeit mit Spring ? Wie soll ich meiner Bohne eine verspottete Abhängigkeit hinzufügen?

Sie können dies bis zu meiner Frage überspringen .

Zuallererst, was ich gelernt habe. Dies ist ein sehr guter Artikel über Mocks Aren't Stubs , in dem die Grundlagen erläutert werden (Mock überprüft das Verhalten und nicht den Status ). Dann gibt es hier ein gutes Beispiel, Mockito, und hier, leichter mit Mockito verspotten . Wir haben eine Erklärung, dass Mockitos Scheinobjekte sowohl Schein- als auch Stummelobjekte sind .

Hier Mockito und hier Matchers finden Sie weitere Beispiele.

Dieser Test

@Test
public void testReal(){
    List<String> mockedList = mock(List.class);
     //stubbing
     //when(mockedList.get(0)).thenReturn("first");

    mockedList.get(anyInt());
    OngoingStubbing<String> stub= when(null);
    stub.thenReturn("first");

    //String res = mockedList.get(0);
                //System.out.println(res);

     //you can also verify using argument matcher
     //verify(mockedList).get(anyInt());

    verify(mockedList);
    mockedList.get(anyInt());
}

funktioniert gut.

Zurück zu meiner Frage. Hier Mockito-Mocks in eine Spring Bean injizieren, versucht jemand, Springs zu verwenden ReflectionTestUtils.setField(), aber hier haben wir Spring-Integrationstests, Erstellen von Mock-Objekten. Wir empfehlen, den Spring-Kontext zu ändern .

Ich habe die letzten beiden Links nicht wirklich verstanden ... Kann mir jemand erklären, welches Problem Spring mit Mockito hat? Was ist los mit dieser Lösung?

@InjectMocks
private MyTestObject testObject

@Mock
private MyDependentObject mockedObject

@Before
public void setup() {
        MockitoAnnotations.initMocks(this);
}

https://stackoverflow.com/a/8742745/1137529

EDIT : Ich war nicht wirklich klar. Ich werde 3 Beispiele für Code bereitstellen, um mich selbst zu verdeutlichen: Angenommen, wir haben Bean HelloWorld mit Methode printHello()und Bean HelloFacade mit Methode sayHello, die Aufrufe an die HelloWorld-Methode weiterleiten printHello().

Das erste Beispiel ist die Verwendung des Spring-Kontexts und ohne benutzerdefinierten Runner unter Verwendung von ReflectionTestUtils für die Abhängigkeitsinjektion (DI):

public class Hello1Test  {
private ApplicationContext ctx;

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
    this.ctx = new ClassPathXmlApplicationContext("META-INF/spring/ServicesImplContext.xml");
}



@Test
public void testHelloFacade() {
    HelloFacade obj = (HelloFacade) ctx.getBean(HelloFacadeImpl.class);
    HelloWorld mock = mock(HelloWorld.class);
    doNothing().when(mock).printHello();

    ReflectionTestUtils.setField(obj, "hello", mock);
    obj.sayHello();

    verify(mock, times(1)).printHello();
}

}

Wie @Noam hervorhob, gibt es eine Möglichkeit, es ohne expliziten Aufruf auszuführen MockitoAnnotations.initMocks(this);. Ich werde in diesem Beispiel auch den Kontext des Frühlings verwenden.

@RunWith(MockitoJUnitRunner.class)
public class Hello1aTest {


@InjectMocks
private HelloFacade obj =  new HelloFacadeImpl();

@Mock
private HelloWorld mock;


@Test
public void testHelloFacade() {
    doNothing().when(mock).printHello();
    obj.sayHello();
}

}

Ein anderer Weg, dies zu tun

public class Hello1aTest {

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
}


@InjectMocks
private HelloFacadeImpl obj;

@Mock
private HelloWorld mock;


@Test
public void testHelloFacade() {
    doNothing().when(mock).printHello();
    obj.sayHello();
}

}

Nichts, dass wir in einem früheren Beispiel HelloFacadeImpl manuell instanziieren und HelloFacade zuweisen müssen, da HelloFacade eine Schnittstelle ist. Im letzten Beispiel können wir einfach HelloFacadeImpl deklarieren und Mokito wird es für uns instanziieren. Der Nachteil dieses Ansatzes besteht darin, dass die zu testende Einheit jetzt eine implizite Klasse und keine Schnittstelle ist.

alexsmail
quelle
1
Stimmt etwas mit der Lösung nicht? Der Blog-Beitrag, auf den Sie verlinken, wird nicht verwendet @InjectMocks(relativ neu, obwohl IIRC vor diesem Blog-Beitrag). Daher kann es vorkommen, dass die Bean-Definitionen neu angeordnet werden müssen. Ich bin mir nicht sicher, was die Frage letztendlich ist.
Dave Newton
Der Frühling hat kein Problem mit Mockito. Oder umgekehrt.
Rob Kielty
Ich glaube, in den meisten Fällen sollten Sie anhand der tatsächlichen Implementierung anstelle der Schnittstelle testen.
Adrian Shum
Ich habe eine neue Frage zu diesem Problem geöffnet. Stackoverflow.com/questions/10937763/…
alexsmail

Antworten:

55

Ehrlich gesagt bin ich mir nicht sicher, ob ich Ihre Frage wirklich verstehe: PI wird versuchen, so viel wie möglich zu klären, was ich aus Ihrer ursprünglichen Frage erhalte:

Erstens sollten Sie in den meisten Fällen KEINE Bedenken hinsichtlich des Frühlings haben. Beim Schreiben Ihres Unit-Tests muss selten eine Feder beteiligt sein. Im Normalfall müssen Sie nur das zu testende System (SUT, das zu testende Ziel) in Ihrem Komponententest instanziieren und auch Abhängigkeiten von SUT in den Test einfügen. Die Abhängigkeiten sind normalerweise ein Mock / Stub.

Ihr ursprünglich vorgeschlagener Weg und Beispiel 2, 3 machen genau das, was ich oben beschreibe.

In einigen seltenen Fällen (z. B. Integrationstests oder spezielle Komponententests) müssen Sie einen Spring-App-Kontext erstellen und Ihr SUT aus dem App-Kontext abrufen. In einem solchen Fall glaube ich, dass Sie:

1) Erstellen Sie Ihre SUT in Spring App CTX, beziehen Sie sich darauf und injizieren Sie Mocks

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @Autowired
    @InjectMocks
    TestTarget sut;

    @Mock
    Foo mockFoo;

    @Before
    /* Initialized mocks */
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void someTest() {
         // ....
    }
}

oder

2) Befolgen Sie die Anweisungen in Ihrem Link Spring Integration Tests, Erstellen von Scheinobjekten . Dieser Ansatz besteht darin, Mocks im App-Kontext von Spring zu erstellen, und Sie können das Mock-Objekt von der App ctx abrufen, um Ihre Stubbing / Verifizierung durchzuführen:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @Autowired
    TestTarget sut;

    @Autowired
    Foo mockFoo;

    @Test
    public void someTest() {
         // ....
    }
}

Beide Wege sollten funktionieren. Der Hauptunterschied besteht darin, dass im ersteren Fall die Abhängigkeiten nach dem Durchlaufen des Lebenszyklus des Frühlings usw. (z. B. Bohneninitialisierung) injiziert werden, während im letzteren Fall vor den Händen injiziert wird. Wenn Ihr SUT beispielsweise die InitializingBean von spring implementiert und die Initialisierungsroutine die Abhängigkeiten umfasst, sehen Sie den Unterschied zwischen diesen beiden Ansätzen. Ich glaube, es gibt kein Richtig oder Falsch für diese beiden Ansätze, solange Sie wissen, was Sie tun.

Nur eine Ergänzung, @Mock, @Inject, MocktoJunitRunner usw. sind bei der Verwendung von Mockito nicht erforderlich. Sie sind nur Dienstprogramme, mit denen Sie die Eingabe von Mockito.mock (Foo.class) und einer Reihe von Setter-Aufrufen ersparen können.

Adrian Shum
quelle
@Noam, werfen Sie einen Blick auf diese Antwort. Adrian, tolle Antwort, danke. :-)
Alexsmail
2
Ich denke nicht, dass 1) funktioniert. Das heißt: Wenn ich beide @Autowiredund benutze @InjectMocks, sehe ich die Bohnen mit Spring-Injektion, nicht die Mocks TestTarget. (Ich hatte gehofft, dass die Verwendung beider Annotationen nur ein Mock für Fooalle anderen Abhängigkeiten injizieren würde , aber dennoch die Standard-Beans mit Spring-Injection für alle anderen Abhängigkeiten verwenden würde, die automatisch verkabelt TestTargetwerden, aber im Integrationstest nicht verspottet werden. Keine Zigarre, wie es scheint. Frühling 3.1.2; Mockito 1.9.5)
Arjan
Obwohl ich das nicht ausprobiert habe, glaube ich, dass es funktionieren sollte. @Autowiredwird vom Spring JUnit Runner behandelt, der zuvor behandelt wurde setup(). @InjectMockswird von MockitoAnnotaitons.initMocks(this)in behandelt setup(). Ich sehe keinen Grund, damit aufzuhören. Ich kann es versuchen, um zu bestätigen, ob es funktioniert. :)
Adrian Shum
7
Ich habe das MockitoAnnotations.initMocks(this)in die aufgenommen @Before. Auch das Entfernen @Autowired(daher nur das @InjectMocksPlatzieren an Ort und Stelle) gibt mir den Schein TestTarget. (Durch das Entfernen bleiben jedoch @Autowiredauch alle anderen Bohnen bis zum Frühjahr nicht initialisiert.) Weitere Untersuchungen zeigen jedoch, dass ich eine TestTarget#setFoo(Foo f)Methode benötige . Ohne @InjectMocksfunktioniert es gut, wenn nicht mit kombiniert @Autowired. Also: Wenn Sie beide @Autowiredund verwenden @InjectMocks, @Autowired private Foo mockFoo;reicht dies nicht aus. Benötigt möglicherweise einen Fehlerbericht. werde untersuchen.
Arjan
1
Leider spielt es keine Rolle, ob man @InjectMocks TestTarget sutoder verwendet @Autowired @InjectMocks TestTarget sut. Bei ersteren injiziert Spring TestTarget(wie erwartet) nichts und es reicht aus, wenn TestTargetprivate Eigenschaften definiert werden, damit Mockito seine Bohnen injiziert. Für letztere TestTargetbraucht ein öffentlicher Setter für jede Bohne, die Mockito injizieren soll. Unter Verwendung des letzteren für ein Ziel , das sich nicht eine öffentliche Setter definieren Foowird mir gerade die Frühling injiziert Bohnen in TestTarget, wo die Feder injiziert Foowird nicht durch die Mockito Mock ersetzt.
Arjan
6

Ihre Frage scheint zu fragen, welches der drei von Ihnen angegebenen Beispiele der bevorzugte Ansatz ist.

Beispiel 1 mit den Reflection TestUtils ist kein guter Ansatz für Unit-Tests . Sie möchten den Federkontext für einen Komponententest überhaupt nicht laden. Verspotten Sie einfach und injizieren Sie, was erforderlich ist, wie in Ihren anderen Beispielen gezeigt.

Sie möchten den Spring-Kontext laden, wenn Sie einige Integrationstests durchführen möchten. Ich würde es jedoch vorziehen @RunWith(SpringJUnit4ClassRunner.class), das Laden des Kontexts zusammen mit zu verwenden, @Autowiredwenn Sie explizit Zugriff auf seine Beans benötigen.

Beispiel 2 ist ein gültiger Ansatz, und durch die Verwendung von @RunWith(MockitoJUnitRunner.class)entfällt die Angabe einer @ Before-Methode und eines expliziten Aufrufs vonMockitoAnnotations.initMocks(this);

Beispiel 3 ist ein weiterer gültiger Ansatz, der nicht verwendet wird @RunWith(...). Sie haben Ihre zu testende Klasse nicht HelloFacadeImplexplizit instanziiert , aber Sie hätten dasselbe mit Beispiel 2 tun können.

Mein Vorschlag ist, Beispiel 2 für Ihre Unit-Tests zu verwenden, da dies die Code-Unordnung verringert. Sie können auf die ausführlichere Konfiguration zurückgreifen, wenn Sie dazu gezwungen werden.

Brad
quelle
4

Mit der Einführung einiger neuer Testfunktionen in Spring 4.2.RC1 können Sie Spring-Integrationstests schreiben, die nicht auf dem basieren SpringJUnit4ClassRunner. Lesen Sie diesen Teil der Dokumentation.

In Ihrem Fall könnten Sie Ihren Spring-Integrationstest schreiben und trotzdem folgende Mocks verwenden:

@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @ClassRule
    public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    @Autowired
    @InjectMocks
    TestTarget sut;

    @Mock
    Foo mockFoo;

    @Test
    public void someTest() {
         // ....
    }
}
geoand
quelle
1
Hier youtube.com/watch?v=-_aWK8T_YMI kann man ein Video über Spring Framework auf Java 8 sehen.
alexsmail
2

Sie brauchen das nicht wirklich, MockitoAnnotations.initMocks(this);wenn Sie Mockito 1.9 (oder neuer) verwenden - alles, was Sie brauchen, ist Folgendes:

@InjectMocks
private MyTestObject testObject;

@Mock
private MyDependentObject mockedObject;

Die @InjectMocksAnnotation fügt dem MyTestObjectObjekt alle Ihre Mocks hinzu .

Noam
quelle
10
Durch die Verwendung @RunWith(MockitoJUnitRunner.class)entfällt die Notwendigkeit, MockitoAnnotations.initMocks(this)explizit aufzurufen . Wie hier beschrieben, ist die Anmerkung seit 1.8.3
Brad
1
@Brad Laufen mit dem Mockito-Läufer ist jedoch nicht immer möglich, das ist alles.
Dave Newton
2

Hier ist meine kurze Zusammenfassung.

Wenn Sie einen Komponententest schreiben möchten, verwenden Sie keinen Spring applicationContext, da in der Klasse, in der Sie den Komponententest durchführen, keine echten Abhängigkeiten eingefügt werden sollen. Verwenden Sie stattdessen Mocks, entweder mit der @RunWith(MockitoJUnitRunner.class)Annotation über der Klasse oder mit MockitoAnnotations.initMocks(this)der Methode @Before.

Wenn Sie einen Integrationstest schreiben möchten, verwenden Sie:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("yourTestApplicationContext.xml")

So richten Sie beispielsweise Ihren Anwendungskontext mit einer In-Memory-Datenbank ein. Normalerweise verwenden Sie in Integrationstests keine Mocks, aber Sie können dies mithilfe des MockitoAnnotations.initMocks(this)oben beschriebenen Ansatzes tun .

Adriaan Koster
quelle
0

Der Unterschied, ob Sie Ihr mit @InjectMocksAnmerkungen versehenes Feld instanziieren müssen, liegt in der Version von Mockito, nicht darin, ob Sie den MockitoJunitRunner oder verwenden MockitoAnnotations.initMocks. In 1.9, das auch einige Konstruktorinjektionen Ihrer @MockFelder behandelt, wird die Instanziierung für Sie durchgeführt. In früheren Versionen müssen Sie es selbst instanziieren.

So mache ich Unit-Tests meiner Spring Beans. Es gibt kein Problem. Menschen geraten in Verwirrung, wenn sie Spring-Konfigurationsdateien verwenden möchten, um die Mocks tatsächlich zu injizieren, was den Punkt von Unit-Tests und Integrationstests überschreitet.

Und natürlich ist das zu testende Gerät ein Impl. Sie müssen eine echte konkrete Sache testen, oder? Selbst wenn Sie es als Schnittstelle deklarieren würden, müssten Sie die reale Sache instanziieren, um es zu testen. Jetzt könnten Sie in Spione geraten, die Stub / Mock-Wrapper um echte Objekte sind, aber das sollte für Eckfälle sein.

Jhericks
quelle
Nun, ich möchte wirklich meinen "Vertrag" testen, der in der Schnittstelle definiert ist. Wenn mein Impl zufällig eine öffentliche Methode hat, die nicht über die Schnittstelle verfügbar gemacht wird, ist dies für mich wie eine private Methode und ich neige dazu, sie zu ignorieren.
Alexsmail
Aber was erfüllt den "Vertrag"? Der Impl, richtig? Das testen Sie also. Wie würde es überhaupt aussehen, nur die Schnittstelle zu testen? Sie können eine Schnittstelle nicht instanziieren.
Jhericks
Ich habe eine neue Frage zu diesem Problem geöffnet. Stackoverflow.com/questions/10937763/…
alexsmail
OK, aus Ihrer anderen Frage geht hervor, dass wir uns nur mit Vokabeln oder Ähnlichem verwechselt haben. Natürlich ist die Instanz, die Sie testen, das Impl, aber deklarieren Sie es als Schnittstelle oder Impl? Ich denke, das ist kontextspezifisch, aber es ist sicherlich weniger unsinnig als das, was ich ursprünglich gedacht hatte.
Jhericks
0

Wenn Sie Ihr Projekt auf Spring Boot 1.4 migrieren würden, könnten Sie neue Anmerkungen @MockBeanzum Fälschen verwenden MyDependentObject. Mit dieser Funktion können Sie Mockitos @Mockund @InjectMocksAnmerkungen aus Ihrem Test entfernen .

luboskrnac
quelle