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
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.