MockK: una libreria beffarda per Kotlin

1. Panoramica

In questo tutorial, daremo un'occhiata ad alcune delle funzionalità di base della libreria MockK.

2. MockK

In Kotlin, tutte le classi e i metodi sono definitivi. Sebbene questo ci aiuti a scrivere codice immutabile, causa anche alcuni problemi durante il test.

La maggior parte delle librerie fittizie JVM ha problemi con il mocking o lo stub delle classi finali. Naturalmente, possiamo aggiungere la parola chiave " open " alle classi e ai metodi che vogliamo deridere. Ma cambiare classe solo per deridere del codice non sembra l'approccio migliore.

Ecco la libreria MockK, che offre supporto per le funzionalità e i costrutti del linguaggio Kotlin. MockK crea proxy per classi derise. Ciò causa un degrado delle prestazioni, ma i vantaggi generali che MockK ci offre ne valgono la pena.

3. Installazione

L'installazione è il più semplice possibile. Dobbiamo solo aggiungere la dipendenza mockk al nostro progetto Maven:

 io.mockk mockk 1.9.3 test 

Per Gradle, dobbiamo aggiungerlo come dipendenza di test:

testImplementation "io.mockk:mockk:1.9.3"

4. Esempio di base

Creiamo un servizio che vorremmo prendere in giro:

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

Ecco un esempio di test che prende in giro TestableService :

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

Per definire l'oggetto mock, abbiamo utilizzato il metodo mockk () .

Nella fase successiva, abbiamo definito il comportamento del nostro mock. A questo scopo, abbiamo creato un blocco every che descrive quale risposta deve essere restituita per quale chiamata.

Infine, abbiamo utilizzato la verifica del blocco di verificare se il mock è stato richiamato come ci aspettavamo .

5. Esempio di annotazione

È possibile utilizzare le annotazioni MockK per creare tutti i tipi di mock. Creiamo un servizio che richiede due istanze del nostro TestableService :

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

InjectTestService contiene due campi con lo stesso tipo. Non sarà un problema per MockK. MockK cerca di abbinare le proprietà per nome, quindi per classe o superclasse. Inoltre, non ha alcun problema con l'iniezione di oggetti in campi privati .

Facciamo finta InjectTestService in un test usando le annotazioni:

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

Nell'esempio precedente, abbiamo utilizzato l' annotazione @InjectMockKs . Questo specifica un oggetto in cui devono essere iniettati i mock definiti. Per impostazione predefinita, inietta variabili che non sono ancora assegnate. Possiamo usare @OverrideMockKs per sostituire i campi che hanno già un valore.

MockK richiede che MockKAnnotations.init (…) venga chiamato su un oggetto che dichiara una variabile con annotazioni. Per Junit5, può essere sostituito con @ExtendWith (MockKExtension :: class) .

6. Spia

La spia consente di prendere in giro solo una parte particolare di una classe. Ad esempio, può essere utilizzato per simulare un metodo specifico in TestableService:

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

Nell'esempio, abbiamo utilizzato il metodo spyk per creare un oggetto spia. Avremmo potuto anche usare l' annotazione @SpyK per ottenere lo stesso risultato:

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

7. Simulazione rilassata

Un tipico oggetto deriso genererà MockKException se proviamo a chiamare un metodo in cui il valore restituito non è stato specificato.

Se non vogliamo descrivere il comportamento di ogni metodo, allora possiamo usare una simulazione rilassata. Questo tipo di simulazione fornisce valori predefiniti per ciascuna funzione. Ad esempio, il tipo restituito String restituirà una stringa vuota . Ecco un breve esempio:

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

Nell'esempio, abbiamo utilizzato il metodo mockk con l' attributo rilassato per creare un oggetto mock rilassato. Avremmo potuto usare anche l' annotazione @RelaxedMockK :

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

8. Object Mock

Kotlin fornisce un modo semplice per dichiarare un singleton utilizzando la parola chiave object :

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

Tuttavia, la maggior parte delle librerie beffarde ha un problema con le istanze singleton di Kotlin. Per questo motivo, MockK fornisce il metodo mockkObject . Diamo un'occhiata:

@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. Derisione gerarchica

Un'altra caratteristica utile di MockK è la capacità di deridere gli oggetti gerarchici. Per prima cosa, creiamo una struttura di oggetti gerarchica:

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 questo articolo, abbiamo discusso le caratteristiche più importanti di MockK. MockK è una potente libreria per il linguaggio Kotlin e fornisce molte funzioni utili. Per ulteriori informazioni su MockK, possiamo controllare la documentazione sul sito Web di MockK.

Come sempre, il codice di esempio presentato è disponibile più volte su GitHub.