Testes Unitários no Node.js: InMemory Repositories, Spies e TDD
Como isolar regras de negócio com InMemory Repositories em vez de mocks com jest.fn(), uso avançado de jest.spyOn() para validação de fluxos, Arrange-Act-Assert (AAA) e cobertura de branches de erro.
Se para testar a sua API você precisa subir o banco de dados, o Redis e enviar um e-mail de verdade, você está fazendo Testes de Integração ou E2E. Eles são úteis, mas são lentos (demoram minutos) e difíceis de cobrir todos os cenários de erro. Testes Unitários testam a regra de negócio em completo isolamento, executando milhares de testes por segundo. E o segredo para isso não é espalhar jest.fn() por todo o código — é usar Test Doubles inteligentes.
O Padrão: InMemoryRepository > jest.fn()
O InMemoryRepository resolve um problema estrutural dos mocks gerados com jest.fn(): estado. Quando o Use Case chama usersRepo.create(data) e depois usersRepo.findByEmail(email), um mock simples precisaria orquestrar dois retornos independentes. O InMemory Repository simplesmente usa um array em memória — create empurra no array, findByEmail busca no array. O comportamento real do repositório é preservado sem o custo de I/O.
Muitos tutoriais ensinam a mockar o repositório usando jest.mock(). O problema é que isso acopla o teste à implementação e não mantém estado. Se o Use Case buscar o usuário e depois tentar salvá-lo, você terá que orquestrar retornos complexos. A abordagem profissional é usar um InMemory Repository: uma classe real que implementa a interface do repositório, mas salva os dados em um array local.
import type { IUsersRepository } from '../../../domain/repositories/IUsersRepository';
import type { User } from '../../../domain/entities/User';
export class InMemoryUsersRepository implements IUsersRepository {
public items: Array<User> = [];
async findByEmail(email: string): Promise<User | null> {
return this.items.find((u) => u.email === email) ?? null;
}
async create(data: Partial<User>): Promise<User> {
const user = { ...data, id: crypto.randomUUID() } as User;
this.items.push(user);
return user;
}
// save, delete, etc., manipulando o array this.items
}Estruturando o Teste Unitário (AAA)
Com os Fakes/InMemory em mãos, injetamos as dependências falsas no Use Case via construtor (a mesma interface que a versão de produção usa). O padrão AAA (Arrange, Act, Assert) divide o teste em três blocos claros: preparar o estado inicial, executar a ação, e verificar o resultado. Um bom teste unitário com essa estrutura é legível como prosa — qualquer pessoa do time entende o que está sendo validado sem precisar ler a implementação.
import { CreateUserUseCase } from './CreateUserUseCase';
import { InMemoryUsersRepository } from '../../../infra/tests/repositories/InMemoryUsersRepository';
import { FakeMailProvider } from '../../../infra/tests/providers/FakeMailProvider';
import { AppError } from '../../../errors/AppError';
// Variáveis declaradas no escopo do suite para acesso nos testes
let usersRepo: InMemoryUsersRepository;
let mailProvider: FakeMailProvider;
let sut: CreateUserUseCase; // SUT = System Under Test
describe('CreateUserUseCase', () => {
// beforeEach garante um estado limpo antes de cada teste
beforeEach(() => {
usersRepo = new InMemoryUsersRepository();
mailProvider = new FakeMailProvider();
// Injeção de dependência manual para o teste
sut = new CreateUserUseCase(usersRepo, mailProvider);
});
it('should be able to create a new user', async () => {
// 1. Arrange (Preparação)
const dto = { name: 'John', email: 'john@doe.com', password: '123' };
// 2. Act (Ação)
const user = await sut.execute(dto);
// 3. Assert (Verificação)
expect(user).toHaveProperty('id');
expect(user.email).toBe('john@doe.com');
expect(user.passwordHash).not.toBe('123'); // Verifica se hasheou
// Verifica se salvou no repositório em memória
expect(usersRepo.items).toHaveLength(1);
});
it('should not allow creating a user with an existing email', async () => {
// Arrange: pré-popula o banco falso
await usersRepo.create({ name: 'Jane', email: 'john@doe.com', passwordHash: 'hash' });
// Act & Assert
await expect(
sut.execute({ name: 'John', email: 'john@doe.com', password: '123' })
).rejects.toBeInstanceOf(AppError);
});
});Validando Efeitos Colaterais com Spies
E se quisermos garantir que o e-mail de boas-vindas foi enviado corretamente? O provedor de e-mail falso não envia nada, mas podemos "espionar" seus métodos com jest.spyOn().
it('should send a welcome email after user creation', async () => {
// Espiona o método sendMail do provedor de email falso
const sendMailSpy = jest.spyOn(mailProvider, 'sendMail');
await sut.execute({
name: 'John',
email: 'john@doe.com',
password: '123'
});
// Verifica se a função espiã foi chamada com os parâmetros corretos
expect(sendMailSpy).toHaveBeenCalledWith({
to: 'john@doe.com',
subject: 'Bem-vindo!',
templateId: 'welcome_template'
});
});Cuidado com mocks excessivos: Testes muito acoplados a jest.fn().mockResolvedValue() quebram facilmente quando a implementação interna muda, mesmo que o comportamento continue correto. Prefira InMemory Repositories (State-based testing) e reserve os Spies apenas para validar limites do sistema (Interaction-based testing), como chamadas a APIs de terceiros ou disparos de eventos.
Conclusão
A Injeção de Dependências torna o código testável: o Use Case não sabe que está recebendo um InMemoryUsersRepository em vez do TypeORMUsersRepository. A suíte de testes roda instantaneamente sem dependências externas, validando a regra de negócio com precisão. Uma cobertura de testes sólida não é sobre atingir 100% de linhas, mas garantir que as regras críticas e os casos de erro estão documentados e blindados contra regressões.