Introdução a Stubs
Ao concluirmos esta aula, nós:
- Saberemos o que é um Stub.
- Compreenderemos como um Stub funciona por dentro.
- Compreenderemos quando devemos utilizar Stubs em nossos testes unitários.
[!NOTE] Esta aula assume que:
O Que É um Stub?
Um Stub é um objeto utilizado para substituir uma dependência externa do Sistema Sob Teste (SUT). Todo stub deve cumprir quatro requisitos fundamentais:
- Um stub deve fornecer a mesma API que a dependência externa substituída: Isso significa que, se a dependência externa for uma classe, nosso stub deve estendê-la e sobrescrever seus métodos. Por outro lado, se a dependência substituída implementar uma interface, nosso stub deve implementar essa mesma interface.
- Devemos ser capazes de configurar a resposta retornada quando um método do stub é invocado: Isso significa que podemos configurar o stub para retornar um objeto específico ou lançar uma exceção.
- Um stub retorna a resposta configurada todas as vezes em que seu método é invocado.
- Um stub não fornece nenhuma maneira de verificar as interações que acontecem entre o Sistema Sob Teste e o próprio stub.
Agora demonstraremos essas regras na prática criando um stub 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 TaskFinder. Essa interface declara um único método chamado findById(), que recebe o ID da tarefa solicitada como parâmetro e retorna o objeto Task encontrado.
O código-fonte da interface TaskFinder em Kotlin é o seguinte:
|
|
Vamos descobrir como podemos criar um stub simples para substituir a interface TaskFinder.
Criando um Stub Simples
Podemos criar um stub capaz de substituir um objeto TaskFinder real seguindo estes três passos:
Passo 1: Criar a classe TaskFinderStub
Primeiro, criamos uma classe pública e final (o padrão em Kotlin) que implementa a interface TaskFinder. É uma boa prática que nossa classe de stub seja final porque isso impede que outros desenvolvedores a estendam e criem hierarquias confusas de dublês de teste.
O código-fonte inicial da nossa classe de stub fica assim:
|
|
[!TIP] Nomenclatura e Visibilidade de Classes de Stub Ao nomear classes de stub manuais, uma convenção clara é acrescentar o sufixo
Stubao nome do tipo do objeto “real”. Como nossa classe de stub é utilizada para substituir objetosTaskFinder, seu nome deve serTaskFinderStub.Além disso, este exemplo assume que precisaremos usar essa classe de stub em múltiplas classes de teste que não estão necessariamente no mesmo pacote. Se ela for usada apenas localmente, podemos restringir sua visibilidade com
internalouprivate.
Passo 2: Configurar o objeto Task retornado
Segundo, precisamos permitir a configuração do objeto Task que será retornado pelo stub. Fazemos isso seguindo estes passos:
- Adicionamos uma propriedade
private val found: Task?à nossa classe de stub. - Definimos o valor dessa propriedade utilizando injeção via construtor.
Depois de adicionarmos o código que nos permite configurar o objeto Task retornado, o código-fonte da nossa classe de stub fica assim:
|
|
Passo 3: Implementar o método findById()
Terceiro, implementamos o método findById() da interface TaskFinder. Nossa implementação deve simplesmente retornar o objeto Task armazenado na propriedade found.
Após implementarmos o método findById(), o código-fonte completo da nossa classe TaskFinderStub fica assim:
|
|
Criamos nosso primeiro stub simples! A seguir, escreveremos um método de teste que demonstra como o nosso stub funciona.
Utilizando o Nosso Stub
Se quisermos utilizar um stub em nossos testes, devemos lembrar que stubs não são stateless (eles carregam o estado da resposta configurada para aquele cenário). Isso significa que devemos criar uma nova instância do stub antes da execução de cada método de teste. Podemos fazer isso adicionando um método de configuração (@BeforeEach) à nossa classe de teste e instanciando nosso stub dentro desse método.
Depois de adicionarmos esse método de setup à nossa classe de teste, o código-fonte fica assim:
|
|
Agora podemos escrever um método de teste que utiliza o nosso stub seguindo estes dois passos:
- Invocamos o método
findById()da interfaceTaskFinder. - Verificamos se o objeto
TaskFinderStubretorna o objetoTaskesperado.
O código-fonte completo do nosso teste fica assim:
|
|
Este exemplo demonstrou na prática que um Stub é um dublê de teste (test double) que:
- Nos permite configurar a resposta que será retornada quando seu método for invocado.
- Retorna a mesma resposta configurada todas as vezes em que seu método é invocado.
- Não fornece nenhuma maneira de verificar as interações que aconteceram entre o Sistema Sob Teste e o stub em questão.
Esses princípios continuam 100% válidos mesmo quando criamos nossos stubs utilizando uma biblioteca de mocking como o MockK! Vamos avançar e identificar as situações em que o uso de stubs faz sentido.
Quando Devemos Usar Stubs?
Normalmente, um Sistema Sob Teste possui dois tipos de dependências externas:
- Dependências que consultam informações utilizadas pelo Sistema Sob Teste (Fontes de Dados / Queries): Essas dependências leem informações do banco de dados utilizado, buscam dados de uma API externa, consultam o relógio do sistema e assim por diante.
- Dependências que disparam uma ação que possui um “efeito colateral” (Side Effect / Commands): Essas dependências salvam ou excluem informações no banco de dados, enviam uma requisição HTTP de mutação para uma API externa, publicam um evento na fila de mensageria e assim por diante.
Como não podemos verificar se uma interação aconteceu entre o Sistema Sob Teste e um stub, devemos utilizar stubs nas situações em que não precisamos verificar se o Sistema Sob Teste realmente invocou um método do stub.
Se compararmos esse argumento com os dois tipos de dependências externas do Sistema Sob Teste, percebemos que:
- Se a dependência for apenas uma fonte de dados (datasource), não precisamos verificar as interações que acontecem entre o Sistema Sob Teste e a dependência externa, pois podemos usar asserções de estado/retorno sobre a saída do próprio Sistema Sob Teste para garantir que ele funcionou como esperado (se o SUT não tivesse consultado a fonte de dados, ele jamais conseguiria devolver o resultado correto).
- Se a dependência disparar uma ação que produz um “efeito colateral”, precisamos verificar se o Sistema Sob Teste invocou o método correto da dependência externa (ou se deixou de invocá-lo quando não deveria). Se não fizermos essa verificação, não teremos como comprovar que o Sistema Sob Teste realizou a ação esperada.
Em outras palavras: devemos substituir uma dependência externa do Sistema Sob Teste por um Stub sempre que essa dependência externa atuar apenas como uma fonte de dados (consulta).
Resumo
Nesta aula, aprendemos quatro conceitos fundamentais:
- Um Stub é um objeto que substitui uma dependência externa do Sistema Sob Teste.
- Um Stub retorna a resposta configurada todas as vezes em que seu método é invocado.
- Um Stub não fornece nenhuma maneira de verificar interações ocorridas entre o Sistema Sob Teste e o próprio stub.
- Devemos substituir uma dependência externa do Sistema Sob Teste por um Stub quando essa dependência externa for apenas uma fonte de consulta de dados (datasource).