MockK: Eine Spottbibliothek für Kotlin

1. Übersicht

In diesem Tutorial werden wir uns einige der Grundfunktionen der MockK-Bibliothek ansehen.

2. MockK

In Kotlin sind alle Klassen und Methoden endgültig. Dies hilft uns zwar beim Schreiben von unveränderlichem Code, verursacht jedoch auch einige Probleme beim Testen.

Die meisten JVM-Mock-Bibliotheken haben Probleme beim Verspotten oder Stubben von Abschlussklassen. Natürlich können wir Klassen und Methoden, die wir verspotten möchten, das Schlüsselwort " open " hinzufügen . Das Ändern von Klassen nur zum Verspotten von Code ist jedoch nicht der beste Ansatz.

Hier kommt die MockK-Bibliothek, die Unterstützung für Kotlin-Sprachfunktionen und -Konstrukte bietet. MockK erstellt Proxys für verspottete Klassen. Dies führt zu Leistungseinbußen, aber die allgemeinen Vorteile, die MockK uns bietet, sind es wert.

3. Installation

Die Installation ist so einfach wie möglich. Wir müssen nur die Mockk-Abhängigkeit zu unserem Maven-Projekt hinzufügen:

 io.mockk mockk 1.9.3 test 

Für Gradle müssen wir es als Testabhängigkeit hinzufügen:

testImplementation "io.mockk:mockk:1.9.3"

4. Grundlegendes Beispiel

Lassen Sie uns einen Service erstellen, den wir verspotten möchten:

class TestableService { fun getDataFromDb(testParameter: String): String { // query database and return matching value } fun doSomethingElse(testParameter: String): String { return "I don't want to!" } }

Hier ist ein Beispieltest, der TestableService verspottet :

@Test fun givenServiceMock_whenCallingMockedMethod_thenCorrectlyVerified() { // given val service = mockk() every { service.getDataFromDb("Expected Param") } returns "Expected Output" // when val result = service.getDataFromDb("Expected Param") // then verify { service.getDataFromDb("Expected Param") } assertEquals("Expected Output", result) }

Um das Mock-Objekt zu definieren, haben wir die mockk () -Methode verwendet.

Im nächsten Schritt haben wir das Verhalten unseres Mocks definiert. Zu diesem Zweck haben wir jeden Block erstellt, der beschreibt, welche Antwort für welchen Aufruf zurückgegeben werden soll.

Schließlich haben wir den Überprüfungsblock verwendet , um zu überprüfen, ob der Mock wie erwartet aufgerufen wurde .

5. Anmerkungsbeispiel

Es ist möglich, MockK-Annotationen zu verwenden, um alle Arten von Mocks zu erstellen. Erstellen wir einen Service, für den zwei Instanzen unseres TestableService erforderlich sind :

class InjectTestService { lateinit var service1: TestableService lateinit var service2: TestableService fun invokeService1(): String { return service1.getDataFromDb("Test Param") } }

InjectTestService enthält zwei Felder mit demselben Typ. Für MockK wird das kein Problem sein. MockK versucht, Eigenschaften nach Namen und dann nach Klasse oder Oberklasse abzugleichen. Es ist auch kein Problem, Objekte in private Felder einzufügen .

Lassen Sie uns InjectTestService in einem Test mithilfe von Anmerkungen verspotten :

class AnnotationMockKUnitTest { @MockK lateinit var service1: TestableService @MockK lateinit var service2: TestableService @InjectMockKs var objectUnderTest = InjectTestService() @BeforeEach fun setUp() = MockKAnnotations.init(this) // Tests here ... }

Im obigen Beispiel haben wir die Annotation @InjectMockKs verwendet . Dies gibt ein Objekt an, in das definierte Mocks injiziert werden sollen. Standardmäßig werden Variablen eingefügt, die noch nicht zugewiesen sind. Wir können @OverrideMockKs verwenden , um Felder zu überschreiben, die bereits einen Wert haben.

Für MockK muss MockKAnnotations.init (…) für ein Objekt aufgerufen werden, das eine Variable mit Anmerkungen deklariert. Für Junit5 kann es durch @ExtendWith (MockKExtension :: class) ersetzt werden .

6. Spionieren

Spy erlaubt es, nur einen bestimmten Teil einer Klasse zu verspotten. Beispielsweise kann damit eine bestimmte Methode in TestableService verspottet werden :

@Test fun givenServiceSpy_whenMockingOnlyOneMethod_thenOtherMethodsShouldBehaveAsOriginalObject() { // given val service = spyk() every { service.getDataFromDb(any()) } returns "Mocked Output" // when checking mocked method val firstResult = service.getDataFromDb("Any Param") // then assertEquals("Mocked Output", firstResult) // when checking not mocked method val secondResult = service.doSomethingElse("Any Param") // then assertEquals("I don't want to!", secondResult) }

Im Beispiel haben wir die Spyk- Methode verwendet, um ein Spionageobjekt zu erstellen. Wir hätten auch die Annotation @SpyK verwenden können , um dasselbe zu erreichen:

class SpyKUnitTest { @SpyK lateinit var service: TestableService // Tests here }

7. Entspannter Mock

Ein typisches verspottetes Objekt löst eine MockKException aus, wenn wir versuchen, eine Methode aufzurufen, bei der der Rückgabewert nicht angegeben wurde.

Wenn wir nicht das Verhalten jeder Methode beschreiben wollen, können wir einen entspannten Mock verwenden. Diese Art von Mock liefert Standardwerte für jede Funktion. Beispielsweise gibt der String- Rückgabetyp einen leeren String zurück . Hier ist ein kurzes Beispiel:

@Test fun givenRelaxedMock_whenCallingNotMockedMethod_thenReturnDefaultValue() { // given val service = mockk(relaxed = true) // when val result = service.getDataFromDb("Any Param") // then assertEquals("", result) }

Im Beispiel haben wir die verwendete mockk Methode mit dem entspannten Attribute zu schaffen ein entspanntes Mock - Objekt. Wir hätten auch die Annotation @RelaxedMockK verwenden können :

class RelaxedMockKUnitTest { @RelaxedMockK lateinit var service: TestableService // Tests here }

8. Objekt verspotten

Kotlin bietet eine einfache Möglichkeit, einen Singleton mithilfe des Objektschlüsselworts zu deklarieren:

object TestableService { fun getDataFromDb(testParameter: String): String { // query database and return matching value } }

Die meisten Verspottungsbibliotheken haben jedoch ein Problem mit dem Verspotten von Kotlin-Singleton-Instanzen. Aus diesem Grund bietet MockK die mockkObject- Methode an. Lass uns einen Blick darauf werfen:

@Test fun givenObject_whenMockingIt_thenMockedMethodShouldReturnProperValue(){ // given mockkObject(TestableService) // when calling not mocked method val firstResult = service.getDataFromDb("Any Param") // then return real response assertEquals(/* DB result */, firstResult) // when calling mocked method every { service.getDataFromDb(any()) } returns "Mocked Output" val secondResult = service.getDataFromDb("Any Param") // then return mocked response assertEquals("Mocked Output", secondResult) }

9. Hierarchisches Verspotten

Ein weiteres nützliches Merkmal von MockK ist die Möglichkeit, hierarchische Objekte zu verspotten. Erstellen wir zunächst eine hierarchische Objektstruktur:

class Foo { lateinit var name: String lateinit var bar: Bar } class Bar { lateinit var nickname: String }

The Foo class contains a field of type Bar. Now, we can mock the structure in just one easy step. Let's mock the name and nickname fields:

@Test fun givenHierarchicalClass_whenMockingIt_thenReturnProperValue() { // given val foo = mockk { every { name } returns "Karol" every { bar } returns mockk { every { nickname } returns "Tomato" } } // when val name = foo.name val nickname = foo.bar.nickname // then assertEquals("Karol", name) assertEquals("Tomato", nickname) }

10. Capturing Parameters

If we need to capture the parameters passed to a method, then we can use CapturingSlot or MutableList. It is useful when we want to have some custom logic in an answer block or we just need to verify the value of the arguments passed. Here is an example of CapturingSlot:

@Test fun givenMock_whenCapturingParamValue_thenProperValueShouldBeCaptured() { // given val service = mockk() val slot = slot() every { service.getDataFromDb(capture(slot)) } returns "Expected Output" // when service.getDataFromDb("Expected Param") // then assertEquals("Expected Param", slot.captured) }

MutableList can be used to capture and store specific argument values for all method invocations:

@Test fun givenMock_whenCapturingParamsValues_thenProperValuesShouldBeCaptured() { // given val service = mockk() val list = mutableListOf() every { service.getDataFromDb(capture(list)) } returns "Expected Output" // when service.getDataFromDb("Expected Param 1") service.getDataFromDb("Expected Param 2") // then assertEquals(2, list.size) assertEquals("Expected Param 1", list[0]) assertEquals("Expected Param 2", list[1]) }

11. Conclusion

In diesem Artikel haben wir die wichtigsten Funktionen von MockK erläutert. MockK ist eine leistungsstarke Bibliothek für die Kotlin-Sprache und bietet viele nützliche Funktionen. Weitere Informationen zu MockK finden Sie in der Dokumentation auf der MockK-Website.

Wie immer ist der dargestellte Beispielcode immer wieder auf GitHub verfügbar.