MockK: uma biblioteca de zombaria para o Kotlin

MockK: uma biblioteca de zombaria para o Kotlin

1. Visão geral

Neste tutorial, vamos dar uma olhada em alguns dos recursos básicos da bibliotecaMockK.

2. MockK

EmKotlin, todas as classes e métodos são finais. Embora isso nos ajude a escrever código imutável, também causa alguns problemas durante o teste.

A maioria das bibliotecas de simulação da JVM tem problemas com zombaria ou stubbing de classes finais. Claro, podemos adicionar a palavra-chave “open” às classes e métodos que queremos simular. Mas mudar as classes apenas para simular algum código não parece ser a melhor abordagem.

Aí vem a biblioteca MockK, que oferece suporte para recursos e construções da linguagem Kotlin. O MockK cria proxies para classes simuladas. Isso causa alguma degradação no desempenho, mas os benefícios gerais que o MockK nos oferece valem a pena.

3. Instalação

A instalação é tão simples quanto possível. Precisamos apenas adicionar a dependênciamockk ao nosso projeto Maven:


    io.mockk
    mockk
    1.9.3
    test

ParaGradle, precisamos adicioná-lo como uma dependência de teste:

testImplementation "io.mockk:mockk:1.9.3"

4. Exemplo básico

Vamos criar um serviço que gostaríamos de imitar:

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

    fun doSomethingElse(testParameter: String): String {
        return "I don't want to!"
    }
}

Aqui está um exemplo de teste que zomba deTestableService:

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

Para definir o objeto simulado, usamos o métodomockk<…>().

Na próxima etapa, definimos o comportamento do nosso mock. Para este propósito, criamos um blocoevery que descreve qual resposta deve ser retornada para qual chamada.

Finalmente, usamos o blocoverify para verificar se o mock foi invocado como esperávamos.

5. Exemplo de anotação

É possível usar anotações do MockK para criar todos os tipos de zombarias. Vamos criar um serviço que requer duas instâncias de nossoTestableService:

class InjectTestService {
    lateinit var service1: TestableService
    lateinit var service2: TestableService

    fun invokeService1(): String {
        return service1.getDataFromDb("Test Param")
    }
}

InjectTestService contém dois campos com o mesmo tipo. Não será um problema para o MockK. O MockK tenta corresponder as propriedades por nome e depois por classe ou superclasse. Ele tambémhas no problem with injecting objects into private fields.

Vamos simularInjectTestService em um teste usando anotações:

class AnnotationMockKUnitTest {

    @MockK
    lateinit var service1: TestableService

    @MockK
    lateinit var service2: TestableService

    @InjectMockKs
    var objectUnderTest = InjectTestService()

    @BeforeEach
    fun setUp() = MockKAnnotations.init(this)

    // Tests here
    ...
}

No exemplo acima, usamos a anotação@InjectMockKs. Isso especifica um objeto onde mocks definidos devem ser injetados. Por padrão, ele injeta variáveis ​​que ainda não foram atribuídas. Podemos usar@OverrideMockKs para substituir os campos que já possuem um valor.

MockK requires MockKAnnotations.init(…) to be called on an object declaring a variable with annotations. ParaJunit5, ele pode ser substituído por@ExtendWith(MockKExtension::class).

6. Spy

Spy allows mocking only a particular part of some class. Por exemplo, pode ser usado para simular um método específico emTestableService:

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

No exemplo, usamos o métodospyk para criar um objeto espião. Também poderíamos ter usado a anotação@SpyK para obter o mesmo:

class SpyKUnitTest {

    @SpyK
    lateinit var service: TestableService

    // Tests here
}

7. Simulação relaxada

Um objeto simulado típico irá lançarMockKException se tentarmos chamar um método onde o valor de retorno não foi especificado.

If we don’t want to describe the behavior of each method, then we can use a relaxed mock. Este tipo de simulação fornece valores padrão para cada função. Por exemplo, o tipo de retornoString retornará umString vazio. Aqui está um pequeno exemplo:

@Test
fun givenRelaxedMock_whenCallingNotMockedMethod_thenReturnDefaultValue() {
    // given
    val service = mockk(relaxed = true)

    // when
    val result = service.getDataFromDb("Any Param")

    // then
    assertEquals("", result)
}

No exemplo, usamos o métodomockk com o atributorelaxed para criar um objeto mock relaxado. Também poderíamos ter usado a anotação@RelaxedMockK:

class RelaxedMockKUnitTest {

    @RelaxedMockK
    lateinit var service: TestableService

    // Tests here
}

8. Object Mock

O Kotlin fornece uma maneira fácil de declarar um singleton usando a palavra-chaveobject:

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

No entanto, a maioria das bibliotecas de zombaria tem um problema ao zombar de instâncias singleton do Kotlin. Por causa disso, o MockK fornece o métodomockkObject. Vamos dar uma olhada:

@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. Zombaria hierárquica

Outro recurso útil do MockK é a capacidade de zombar de objetos hierárquicos. Primeiro, vamos criar uma estrutura hierárquica de objetos:

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

class Bar {
    lateinit var nickname: String
}

A classeFoo contém um campo do tipoBar. Agora, podemos simular a estrutura em apenas uma etapa fácil. Vamos simular os camposname enickname:

@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. Parâmetros de captura

Se precisarmos capturar os parâmetros passados ​​para um método, podemos usarCapturingSlot ouMutableList.. É útil quando queremos ter alguma lógica personalizada em um blocoanswer ou apenas precisamos para verificar o valor dos argumentos passados. Aqui está um exemplo deCapturingSlot:

@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 pode ser usado para capturar e armazenar valores de argumento específicos para todas as invocações de método:

@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. Conclusão

Neste artigo, discutimos os recursos mais importantes do MockK. O MockK é uma biblioteca poderosa para a linguagem Kotlin e fornece muitos recursos úteis. Para mais informações sobre o MockK, podemos verificar odocumentation on the MockK website.

Como sempre, o código de amostra apresentado está disponível emover on GitHub.