Introdução a Fakes em Memória
Ao concluirmos esta aula, nós:
- Saberemos o que é um Fake.
- Compreenderemos como objetos Fake funcionam por dentro.
- Compreenderemos quando devemos utilizar objetos Fake em vez de Stubs ou Mocks.
[!NOTE] Esta aula assume que:
O Que É um Fake?
Um Fake é um objeto utilizado para substituir uma dependência externa do Sistema Sob Teste (SUT). Todo objeto fake deve cumprir dois requisitos essenciais:
- Um fake deve fornecer a mesma API que a dependência externa substituída: Se a dependência externa for uma classe, nossa classe fake deve estendê-la e sobrescrever todos os seus métodos. Se a dependência substituída implementar uma interface, nossa classe fake deve implementar essa mesma interface.
- Um fake fornece uma implementação leve e funcional da dependência real: Ele age exatamente como a dependência verdadeira aos olhos de quem o chama, mas faz concessões (como armazenar dados em memória RAM) que o tornam inadequado para uso no ambiente de produção.
Agora demonstraremos essas regras na prática construindo um fake 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 TaskRepository. Essa interface declara dois métodos que são utilizados para salvar objetos Task no banco de dados e consultá-los de volta:
- O método
findById()recebe o ID da tarefa solicitada como parâmetro e retorna a tarefa encontrada (Task?). - O método
save()recebe a tarefa a ser salva como parâmetro, persiste-a no banco de dados e retorna as informações da tarefa salva (já com seu ID gerado).
O código-fonte da interface TaskRepository em Kotlin é o seguinte:
|
|
Vamos descobrir como podemos criar um objeto fake simples capaz de substituir objetos TaskRepository reais.
Criando um Objeto Fake Simples
Podemos criar um objeto fake que substitui o TaskRepository real seguindo estes quatro passos:
Passo 1: Criar a classe TaskRepositoryFake
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 fake fica assim:
|
|
[!TIP] Nomenclatura de Classes Fake Ao nomear classes fake, uma ótima prática é acrescentar o sufixo
Fakeao nome do tipo do objeto “real”. Como nossa classe substitui objetosTaskRepository, seu nome deve serTaskRepositoryFake.
Passo 2: Criar o mecanismo de armazenamento em memória
Segundo, criamos um mecanismo interno que imita o comportamento do banco de dados real:
- Adicionamos um campo privado
private var nextId: Long = 1Là nossa classe fake. Esse campo contém o ID sequencial que será atribuído à próxima tarefa salva em nosso banco de dados em memória. - Adicionamos um campo
private val tasks = mutableMapOf<Long, Task>()à nossa classe fake. EsseMutableMaparmazena os objetosTasksalvos indexados pelo seu ID.
O código-fonte da classe TaskRepositoryFake fica assim:
|
|
Passo 3: Implementar o método findById()
Terceiro, implementamos o método findById() da interface TaskRepository. Fazemos isso simplesmente buscando o objeto Task solicitado dentro do nosso Map (tasks[id]) e retornando o objeto encontrado.
Após implementarmos esse método, o código-fonte da classe TaskRepositoryFake fica assim:
|
|
Passo 4: Implementar o método save()
Quarto, implementamos o método save() da interface TaskRepository seguindo estes passos:
- Atribuímos o ID correto (
nextId) ao objetoTaskque está sendo salvo. - Inserimos a tarefa no nosso
Map(tasks[id] = saved). - Incrementamos o valor do campo
nextIdem1. - Retornamos o objeto
Tasksalvo.
Após implementarmos o método save(), o código-fonte completo da classe TaskRepositoryFake fica assim:
|
|
Criamos um objeto fake simples e funcional! A seguir, escreveremos um método de teste que demonstra como o nosso fake funciona.
Utilizando o Nosso Objeto Fake
Podemos criar uma classe de teste que utiliza o nosso objeto fake seguindo estes dois passos:
Passo 1: Criar uma nova instância do Fake no @BeforeEach
Primeiro, criamos uma nova instância do nosso objeto fake antes que o método de teste seja invocado, adicionando um método @BeforeEach à nossa classe de teste:
|
|
[!NOTE] Na prática do dia a dia, geralmente não criamos um novo objeto fake manualmente em testes unitários isolados de método a método (onde Stubs e Mocks são mais diretos). O motivo ficará óbvio na próxima seção!
Passo 2: Escrever o método de teste saveAndFindTask()
Segundo, implementamos o nosso método de teste seguindo estes passos:
- Criamos um novo objeto
Task. - Salvamos o objeto
Taskcriado “no banco de dados” (repository.save(newTask)). - Verificamos se o objeto
Taskretornado pelosave()contém o ID (1L) e o título corretos. - Buscamos o objeto
Task“do banco de dados” (repository.findById(...)) usando o ID da tarefa salva como critério de busca. - Verificamos se o objeto
Taskencontrado contém as informações corretas.
Após implementarmos o método de teste, o código-fonte completo da nossa classe de teste fica assim:
|
|
Este exemplo demonstrou duas características fundamentais:
- Nosso método de teste interage com o fake exatamente como interagiria com o
TaskRepositoryreal (sem precisar configurar respostas fixas comevery { ... } returns ...), porque o fake possui lógica de estado funcional própria. - Nenhuma pessoa em sã consciência usaria esse
TaskRepositoryFake(baseado em umHashMapnão sincronizado em RAM que perde dados ao reiniciar e não suporta transações) em código de produção!
Agora vamos descobrir quando devemos usar objetos fake.
Quando Devemos Usar Objetos Fake?
Ao decidirmos se devemos ou não usar um fake, devemos lembrar que um fake fornece uma implementação leve da dependência real. Isso traz dois grandes benefícios:
- Um fake é muito mais fácil de configurar do que a dependência real: Normalmente, fakes exigem zero configuração externa, o que é crucial quando estamos escrevendo testes de integração ou testes end-to-end que já possuem configurações complexas.
- Um fake é ordens de grandeza mais rápido do que a dependência real: Como um fake roda em memória ou localmente sem I/O pesado, podemos fazer concessões de arquitetura que seriam impossíveis em produção.
Para saber exatamente quando escolher um Fake, vamos compará-lo diretamente com Stubs e Mocks:
- Um Stub nos ajuda a controlar exatamente qual resposta será retornada quando seu método for invocado em um cenário pontual.
- Um Mock nos ajuda a controlar o comportamento da dependência substituída e verificar as interações exatas ocorridas entre o Sistema Sob Teste e o mock.
- Um Fake age como a dependência real de ponta a ponta: não queremos configurar manualmente a resposta de cada chamada nem queremos verificar interações de métodos; queremos apenas substituir uma dependência problemática por uma implementação funcional leve.
Por essa razão, devemos usar Fakes principalmente quando estamos escrevendo testes de integração ou testes End-to-End (ou testes de fluxos de casos de uso completos) e queremos substituir uma dependência externa que é difícil demais de configurar ou lenta demais para rodar a cada ciclo:
- Por exemplo, substituir um servidor de e-mail SMTP real por um servidor SMTP fake em memória, substituir um gateway de pagamento externo por um servidor HTTP fake (como WireMock), ou usar um banco de dados em memória leve quando apropriado.
Resumo
Nesta aula, aprendemos quatro pontos essenciais:
- Um Fake é um objeto que substitui uma dependência externa do Sistema Sob Teste.
- Um Fake deve fornecer a mesma API pública que a dependência externa substituída.
- Um Fake age como a implementação real (mantendo estado funcional coerente), mas jamais deve ser usado no ambiente de produção.
- Devemos usar um Fake quando configurar a dependência externa real for inviável ou quando o uso da dependência real deixar nossos testes excessivamente lentos.