O que é?
Mockito é um framework que facilita a criação de mocks - objetos falsos que substituem partes reais do código.
Para aprender mais sobre mocks e seu papel nos testes confira o artigo Mocks: Introdução a Automatização de Testes com Mock Object.
Por que é útil?
Mockito melhora o código de teste, porque torna ele mais legível. Além disso, Mockito também produz mensagens que facilitam rastrear erros.
Características
- Primeiramente escrito para Java, mas foi portado para Kotlin;
- Torna o código mais legível;
- Gera mensagens que facilitam rastrear erros.
Como utilizar: configuração
Configure Mockito em um projeto usando o Gradle, adicionando na sessão dependencies do arquivo build.gradle.kts as seguintes dependências do Código 1.
testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.1")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.3.1")
Note que você ainda precisará do JUnit para usar o Mockito.
Após isso, caso seu projeto ainda não tenha uma sessão de testes configurada, insira uma task, ainda no arquivo build.gradle.kts, conforme o Código 2.
tasks.withType<Test> {
useJUnitPlatform()
}
A partir disso podemos começar a escrever os primeiros mocks com o Mockito.
Exemplo prático
Para ilustrar como criar um teste usando o Mockito criaremos um pequeno projeto com duas classes, conforme mostra a Figura 1. A ideia por trás desse microprojeto é simular um pagamento.
Payment representa o pagamento e PaymentGateway a comunicação entre a aplicação e a empresa que debita o saldo do cartão de crédito do cliente.
Código das classes
A função das classes Payment e PaymentGateway importam mais que seus códigos no contexto deste artigo.
Entendendo o que elas fazem podemos ter uma ideia de como elas fazem.
Sendo assim, os Códigos 3 e 4 demonstram a resposta de seus métodos em pseudocódigo.
open class PaymentGateway {
open fun request() = "Request payment gateway"
}
class Payment(private val paymentGateway: PaymentGateway) {
fun pay() = {
// Calcula o valor final
// Aplica descontos
paymentGateway.request()
}
}
Relação entre Payment e PaymentGateway
O objetivo desse exemplo é apresentar uma classe que contém regras de negócio, Payment, que depende de outra que não possui, PaymentGateway.
Uma vez que PaymentGateway pode falhar, por uma queda na internet, por exemplo, precisamos isolá-la ao testar Payment. Para isso criamos um mock para PaymentGateway. Assim, caso Payment falhe isso não ocorrerá ao acaso.
Criando e validando o mock
Antes de criar um mock precisamos criar uma classe de testes , como mostra o Código 5. No método de teste `Succeeds to pay given a valid payment gateway` inserimos o código de teste.
internal class PaymentTest {
@Test
fun `Succeeds to pay given a valid payment gateway`() {
val mock = mock<PaymentGateway> {
on { request() } doReturn "mock implementation"
}
val payment = Payment(mock)
payment.pay()
verify(mock).request()
}
}
Na Linha 5 criamos o mock usando o método mock do Mockito.
Na Linha 6, estamos substituindo o código real do método PaymentGateway::request() por um código falso. Ele vai fazer com que a string "mock implementation" seja retornada sempre que esse método for chamado.
A instrução on { request() } doReturn "mock implementation", na Linha 6, pode ser lida como "para o método request() quando ele for invocado, retorne 'mock implementation'".
A técnica de criar código falso para os mocks é chamada stubbing.
Na Linha 9 passamos o mock para o construtor de Payment.
Na Linha 10, invocamos o método Payment::pay() do Código 4. Internamente, o método Payment::pay() deve invocar PaymentGateway::request(), fazendo com nosso stub seja executado.
Na Linha 12 fazemos o assertion, a verificação que determina se o teste falhou ou não. Nessa assertion usamos Mockito::verify() para checar se o método PaymentGateway::request() foi invocado, como mostra a Figura 2.
Nesse caso, uma vez que PaymentGateway::request() foi invocado, o teste foi bem-sucedido.
Produzindo e analisando uma falha
Caso se deseje ver o teste falhar, podemos comentar a invocação de PaymentGateway::request() em Payment::pay(), conforme o Código 6.
class Payment(private val paymentGateway: PaymentGateway) {
fun pay() {
// paymentGateway.request()
}
}
Ao fazermos isso, Mockito::verify() saberá que PaymentGateway::request() em Payment não foi invocado, pois está comentado, e falhará o teste com uma mensagem como a apresentada no Código 7 e na Figura 3.
Wanted but not invoked:
paymentGateway.request();
-> at PaymentTest.Test mock(PaymentTest.kt:19)
Actually, there were zero interactions with this mock.
A mensagem nos permite rastrear o erro, uma vez que mostra onde o problema ocorreu, PaymentTest.kt:19", e o que o ocasionou, "Wanted but not invoked: paymentGateway.request();".
Final classes
Anteriormente, usamos o modificador open na classe PaymentGateway apresentado no Código 8.
open class PaymentGateway {
…
}
Mockito não funciona com classes final, padrão do Kotlin, sem alguma configuração.
Se tentarmos executar novamente o teste após remover open da classe PaymentGateway receberemos o erro apresentado no Código 9.
Cannot mock/spy class PaymentGateway
Mockito cannot mock/spy because :
- final class
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class PaymentGateway
Mockito cannot mock/spy because :
- final class
Além de usar open para contornar esse problema, podemos configurar o Mockito de forma que ele saiba o que fazer nessa condição.
Para isso crie uma arquivo chamado org.mockito.plugins.MockMaker na pasta src/test/resources/mockito-extensions com conteúdo do Código 10.
mock-maker-inline
A partir disso podemos remover o modificador open da classe PaymentGateway e nosso código continuará funcionando conforme esperado.
Conclusão
Atualmente, escrevemos mais códigos de testes do que qualquer outro em aplicações. Por este motivo, dominar técnicas triviais de teste como mocking e stubbing é fundamental para o programador. Nesse artigo você aprendeu como usar o Mockito para essa tarefa, escrevendo código de testes mais limpo e que produzem mensagens mais eficientes quanto a rastreabilidade de erros.