Voltar para Artigos
Back-end★ Destaque6 min de leitura

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.

12 de agosto de 2026

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.

InMemoryUsersRepository.ts
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.

CreateUserUseCase.spec.ts
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().

CreateUserUseCase.spec.ts (continuação)
  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.