Introdução a Mocks
Ao concluirmos esta aula, nós:
- Saberemos o que é um Mock.
- Compreenderemos como objetos Mock funcionam por dentro.
- Compreenderemos quando devemos utilizar Mocks em nossos testes unitários.
[!NOTE] Esta aula assume que:
O Que É um Mock?
Um Mock é um objeto utilizado para substituir uma dependência externa do Sistema Sob Teste (SUT). Todo mock deve cumprir cinco requisitos fundamentais:
- Um mock deve fornecer a mesma API que a dependência externa substituída: Se a dependência externa for uma classe, nosso mock deve estendê-la e sobrescrever seus métodos. Se a dependência substituída implementar uma interface, nosso mock deve implementar essa mesma interface.
- Devemos ser capazes de configurar a resposta retornada quando um método do mock é invocado: Podemos configurar o mock tanto para retornar um objeto quanto para lançar uma exceção.
- Um mock retorna a resposta configurada todas as vezes em que seu método é invocado.
- Devemos ser capazes de configurar os valores ou objetos esperados que devem ser passados como parâmetros quando um método do mock for invocado.
- Um mock fornece uma maneira de verificar as interações que acontecem entre o Sistema Sob Teste e o próprio mock.
Agora demonstraremos essas cinco regras na prática construindo um objeto mock simples passo a passo. Vamos começar analisando a dependência do Sistema Sob Teste.
Introdução ao Sistema Sob Teste (SUT)
O nosso Sistema Sob Teste possui uma única dependência chamada TaskRepository. Essa interface declara um método chamado deleteById(), que recebe o ID da tarefa a ser excluída como parâmetro e retorna o objeto Task excluído.
O código-fonte da interface TaskRepository em Kotlin é o seguinte:
|
|
Vamos descobrir como podemos criar um mock simples para a interface TaskRepository.
Criando um Mock Simples
Podemos criar um mock capaz de substituir um objeto TaskRepository real seguindo estes cinco passos:
Passo 1: Criar a classe TaskRepositoryMock
Primeiro, criamos uma classe pública e final (padrão em Kotlin) que implementa a interface TaskRepository.
O código-fonte inicial da nossa classe mock fica assim:
|
|
[!TIP] Nomenclatura de Classes Mock Ao nomear classes mock manuais, acrescentamos o sufixo
Mockao tipo do objeto real. Como nossa classe substitui objetosTaskRepository, seu nome deve serTaskRepositoryMock.
Passo 2: Configurar o objeto Task retornado
Segundo, escrevemos um método (ou propriedade) utilizado para configurar o objeto Task que será retornado quando o método deleteById() for invocado. Implementamos isso seguindo estes passos:
- Adicionamos um campo privado
private var returnedTask: Task? = nullà classeTaskRepositoryMock. Esse campo armazena a referência para o objetoTaskque será retornado. - Adicionamos um método
setReturnedTask(returnedTask: Task?)à classeTaskRepositoryMock. - Implementamos o método
setReturnedTask()atribuindo o objeto recebido ao camporeturnedTask.
Após implementarmos esse método, o código-fonte da nossa classe mock fica assim:
|
|
Passo 3: Configurar o argumento id esperado
Terceiro, escrevemos um método utilizado para configurar o ID esperado que deve ser passado como parâmetro para o método deleteById(). Implementamos isso seguindo estes passos:
- Adicionamos um campo privado
private var expectedIdArgument: Long? = nullà classeTaskRepositoryMock. Esse campo armazena o ID que esperamos receber quandodeleteById()for invocado. - Adicionamos um método
setExpectedIdArgument(expectedIdArgument: Long)à classeTaskRepositoryMock. - Implementamos o método
setExpectedIdArgument()armazenando o ID esperado no campoexpectedIdArgument.
Após implementarmos esse método, o código-fonte da nossa classe mock fica assim:
|
|
Passo 4: Implementar o método deleteById() registrando a chamada
Quarto, implementamos o método deleteById() da interface TaskRepository seguindo estes passos:
- Adicionamos um campo privado
private var actualIdArgument: Long? = nullà nossa classe mock. Esse campo armazena o ID real que foi passado como parâmetro quando o métododeleteById()foi invocado. - Adicionamos um campo privado
private var deleteByIdCalled: Boolean = falseà nossa classe mock. Esse campo é utilizado para determinar se o métododeleteById()foi efetivamente invocado pelo Sistema Sob Teste. - Implementamos o método
deleteById(), atualizando os valores deactualIdArgumentedeleteByIdCalled = true, e retornando o objetoreturnedTaskconfigurado.
Após implementarmos o método deleteById(), o código-fonte da nossa classe mock fica assim:
|
|
Passo 5: Implementar o método verify()
Quinto, implementamos o método utilizado para verificar que o método deleteById() foi invocado e que o ID esperado foi passado como parâmetro:
- Adicionamos um método
verify()à classeTaskRepositoryMock. Esse método não recebe parâmetros e não retorna nada. - Implementamos o método escrevendo as asserções necessárias com o AssertJ para validar tanto
deleteByIdCalledquantoactualIdArgument.
Após implementarmos o método verify(), o código-fonte completo da nossa classe TaskRepositoryMock fica assim:
|
|
Criamos um mock simples! A seguir, escreveremos dois métodos de teste que demonstram como o nosso mock funciona.
Utilizando o Nosso Objeto Mock
Se quisermos utilizar um objeto mock em nossos testes, devemos compreender que objetos mock não são stateless (eles gravam o histórico de chamadas recebidas). Isso significa que devemos criar um novo objeto mock antes que cada método de teste seja invocado, utilizando um método @BeforeEach.
Depois de adicionarmos esse método de setup à nossa classe de teste, o código-fonte fica assim:
|
|
Agora podemos escrever dois métodos de teste que demonstram como nosso objeto mock funciona:
Primeiro Teste: Verificar a Chamada do Método e o Argumento Passado
Escrevemos um método de teste que verifica se o método deleteById() foi invocado e se o ID correto foi passado como parâmetro seguindo estes passos:
- Configuramos o ID que deve ser passado como parâmetro quando o método
deleteById()for invocado (setExpectedIdArgument). - Invocamos o método
deleteById()da interfaceTaskRepository. - Verificamos (
repository.verify()) se o métododeleteById()foi invocado e se o ID esperado foi recebido.
O código-fonte desse teste fica assim:
|
|
Segundo Teste: Verificar o Retorno da Tarefa Excluída
Escrevemos um segundo método de teste que verifica se o objeto Task excluído é retornado quando o método deleteById() é invocado seguindo estes passos:
- Criamos um novo objeto
Task. - Configuramos o objeto
Taskque deve ser retornado quando o métododeleteById()for invocado (setReturnedTask). - Invocamos o método
deleteById()da interfaceTaskRepository. - Escrevemos uma asserção que verifica se o objeto
Taskcorreto foi retornado.
O código-fonte completo com ambos os testes fica assim:
|
|
Este exemplo demonstrou que um Mock é um dublê de teste que:
- Nos permite configurar a resposta que é retornada quando seu método é invocado.
- Retorna a mesma resposta todas as vezes em que seu método é invocado.
- Nos permite configurar os valores ou objetos esperados que devem ser passados como parâmetros de método.
- Fornece uma maneira de verificar as interações ocorridas entre o Sistema Sob Teste e o mock em questão.
Mesmo que nosso objeto mock manual seja simples, exatamente os mesmos princípios fundamentais se aplicam quando utilizamos uma biblioteca moderna de mocking como o MockK.
A seguir, descobriremos quando devemos usar objetos mock.
Quando Devemos Usar Mocks?
Como vimos na aula anterior, um Sistema Sob Teste normalmente possui dois tipos de dependências externas:
- Dependências que consultam informações utilizadas pelo Sistema Sob Teste (Fontes de Dados).
- Dependências que disparam uma ação que possui um “efeito colateral” (Side Effect): salvar ou excluir dados no banco de dados, enviar uma requisição HTTP para uma API externa, disparar um evento de domínio etc.
Como podemos verificar se uma interação aconteceu entre o Sistema Sob Teste e um mock, devemos usar mocks nas situações em que precisamos comprovar que o Sistema Sob Teste realmente invocou (ou deixou de invocar) um método da dependência externa:
- Se a dependência for apenas uma fonte de dados (datasource), não precisamos verificar interações — basta usar um Stub e fazer asserções sobre o retorno do Sistema Sob Teste.
- Se a dependência disparar uma ação que possui um efeito colateral (como
deleteById,save,sendEmail,publishEvent), precisamos verificar que o Sistema Sob Teste invocou o método correto da dependência externa com os argumentos corretos (ou que não o invocou em caso de erro). Sem essa verificação, seria impossível garantir que o efeito colateral desejado ocorreu.
Em outras palavras: devemos substituir uma dependência externa do Sistema Sob Teste por um Mock sempre que essa dependência externa disparar uma ação que produz um “efeito colateral”.
Resumo
Nesta aula, aprendemos quatro conceitos fundamentais:
- Um Mock é um objeto que substitui uma dependência externa do Sistema Sob Teste e retorna a resposta configurada quando seu método é invocado.
- Um Mock nos permite configurar os valores ou objetos esperados que devem ser passados como parâmetros quando um método do mock é chamado.
- Um Mock fornece um mecanismo para verificar as interações ocorridas entre o Sistema Sob Teste e o próprio mock.
- Devemos substituir uma dependência externa do Sistema Sob Teste por um Mock quando essa dependência externa disparar uma ação que causa um “efeito colateral” (side effect).