Introdução a Spies
Ao concluirmos esta aula, nós:
- Saberemos o que é um Spy.
- Compreenderemos como um Spy funciona por dentro.
- Compreenderemos quando devemos utilizar Spies em nossos testes.
[!NOTE] Esta aula assume que:
O Que É um Spy?
Um Spy (espião) é um objeto utilizado para substituir uma dependência externa do Sistema Sob Teste (SUT). Na definição clássica de padrões xUnit, todo spy cumpre os seguintes requisitos:
- Um spy deve fornecer a mesma API que a dependência externa substituída: Se a dependência externa for uma classe, nosso spy deve estendê-la e sobrescrever os métodos que nos interessam. Se a dependência substituída implementar uma interface, nosso spy deve implementar essa mesma interface.
- Um spy puro não fornece maneira de configurar a resposta retornada quando seu método é invocado.
- Um spy não fornece maneira de especificar previamente os valores ou objetos esperados que devem ser passados como parâmetros quando um método do spy é invocado.
- Um spy não possui um método
verify()interno que valida expectativas pré-configuradas: Em vez disso, ele fornece métodos getter (propriedades de leitura) que são usados pelo teste para consultar as informações gravadas pelo spy durante a execução e validá-las com asserções normais. - Um spy grava o histórico de chamadas do método-alvo e os parâmetros passados nessas chamadas (podendo gravar a contagem de chamadas, a lista de argumentos recebidos, ou ambos).
Agora demonstraremos essas regras na prática criando um spy simples passo a passo. Vamos começar conhecendo a interface de dependência do nosso Sistema Sob Teste.
Introdução ao Sistema Sob Teste (SUT)
O nosso Sistema Sob Teste possui uma única dependência chamada TaskAuditLog. Essa interface declara um único método chamado logTask(), que recebe um objeto Task como parâmetro e não retorna nada (Unit).
O código-fonte da interface TaskAuditLog em Kotlin é o seguinte:
|
|
Vamos descobrir como podemos criar um spy simples para a interface TaskAuditLog.
Criando um Spy Simples
Podemos criar um spy capaz de substituir objetos TaskAuditLog reais seguindo estes quatro passos:
Passo 1: Criar a classe TaskAuditLogSpy
Primeiro, criamos uma classe pública e final (padrão em Kotlin) que implementa a interface TaskAuditLog.
O código-fonte inicial da nossa classe spy fica assim:
|
|
[!TIP] Nomenclatura de Classes Spy Ao nomear classes spy manuais, acrescentamos o sufixo
Spyao nome do tipo do objeto “real”. Como nossa classe substitui objetosTaskAuditLog, seu nome deve serTaskAuditLogSpy.
Passo 2: Criar o mecanismo que grava o histórico de invocações
Segundo, criamos os campos que registram o histórico de chamadas do método logTask():
- Adicionamos um campo
invocationCount: Int = 0à classe spy. Esse campo registra quantas vezes o métodologTask()foi invocado pelo Sistema Sob Teste. - Adicionamos uma lista
auditedTasks: MutableList<Task> = mutableListOf()à classe spy. Essa lista armazena todos os objetosTaskque foram passados como parâmetro para o métodologTask().
Após implementarmos essa estrutura, o código-fonte da classe TaskAuditLogSpy fica assim:
|
|
Passo 3: Implementar o método logTask()
Terceiro, implementamos o método logTask() da interface TaskAuditLog seguindo estes dois passos:
- Incrementamos o valor do campo
invocationCountem1. - Adicionamos o objeto
Taskrecebido à listaauditedTasks.
Após implementarmos o método logTask(), o código-fonte da classe TaskAuditLogSpy fica assim:
|
|
Passo 4: Expor os métodos de leitura (getters) das informações gravadas
Quarto, implementamos os métodos getter utilizados pelo teste para acessar as informações registradas pelo nosso spy:
- O método
getInvocationCount()retorna a quantidade de vezes que o métodologTask()foi invocado. - O método
getAuditedTasks()retorna a lista de objetosTaskque foram passados como parâmetro para o métodologTask().
Após implementarmos esses métodos de acesso, o código-fonte completo da classe TaskAuditLogSpy fica assim:
|
|
Criamos nosso spy simples! A seguir, escreveremos um método de teste que demonstra como o nosso spy funciona.
Utilizando o Nosso Spy
Se quisermos usar um spy em nossos testes, devemos lembrar que spies não são stateless (eles acumulam o histórico de chamadas). Portanto, devemos criar uma nova instância do spy 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 nosso método de teste seguindo estes quatro passos:
- Criamos um novo objeto
Task. - Invocamos o método
logTask()da interfaceTaskAuditLog. - Garantimos com uma asserção do AssertJ que o método
logTask()foi invocado exatamente uma vez (getInvocationCount()). - Garantimos com uma asserção do AssertJ que o objeto
Taskcriado foi passado como parâmetro (getAuditedTasks()).
Após implementarmos nosso método de teste, o código-fonte completo da classe de teste fica assim:
|
|
Este exemplo demonstrou que um Spy é um dublê de teste que:
- Monitora as invocações do método-alvo.
- Pode gravar a contagem de chamadas do método-alvo e salvar os objetos passados como parâmetros para inspeção posterior.
- Fornece métodos de leitura (getters) para acessar as informações gravadas.
- Nos permite usar asserções padrão (AssertJ) sobre os dados gravados, em vez de configurar expectativas prévias como fazíamos no Mock manual.
A seguir, aprenderemos a identificar as situações em que devemos utilizar spies.
Quando Devemos Usar Spies?
Se estivermos desenvolvendo um projeto novo do zero (greenfield development), na maioria das vezes usaremos Stubs e Mocks. Dito isso, existem três situações específicas em que o padrão Spy é muito útil:
- Primeiro, quando precisamos verificar as interações entre o Sistema Sob Teste e nosso dublê de teste, mas não sabemos de antemão exatamente quantas vezes o método-alvo será invocado ou quais valores exatos serão passados antes de inspecioná-los em conjunto.
- Segundo, quando é difícil reportar uma expectativa falha de um mock de volta para o executor de testes (test runner) — por exemplo, se o Sistema Sob Teste estiver executando em outra thread, corrotina assíncrona ou dentro de um container separado, onde inspecionar o estado gravado pelo spy ao final é muito mais seguro.
- Terceiro, quando preferimos usar asserções do AssertJ (por exemplo, capturando argumentos ou inspecionando listas gravadas no spy) porque queremos customizar a mensagem de erro ou sentimos que asserções explícitas revelam melhor a intenção do teste do que expectativas de mock. (Além disso, em bibliotecas como o MockK, veremos também o uso de
spyk()para envolver objetos reais existentes monitorando suas chamadas).
Resumo
Nesta aula, aprendemos seis pontos importantes:
- Um Spy é um objeto que substitui (ou envolve) uma dependência do Sistema Sob Teste para gravar as interações recebidas.
- Na definição clássica de Meszaros, um spy manual não configura respostas prévias nem expectativas de argumentos antes da chamada.
- Em vez de um método
verify()fechado, um spy expõe as informações gravadas (contagem de chamadas e argumentos recebidos) para que escrevamos asserções com o AssertJ ao final do teste. - Um spy é um dublê de teste usado em cenários específicos onde capturar os argumentos para asserções customizadas ou espionar objetos reais traz mais clareza do que usar um mock convencional.