Dominando Testes Unitários Avançados com Vitest: Mocks, Spies e Stubs em Projetos Reais Já leu

Introdução: Por que Mocks, Spies e Stubs são Essenciais Testes unitários avançados vão além de simples assertions. Quando você testa uma função que depende de APIs externas, bancos de dados ou módulos complexos, precisa de técnicas sofisticadas para isolar o comportamento do código sob teste. Mocks, spies e stubs são as ferramentas que permitem controlar dependências, simular comportamentos e validar interações entre componentes. O Vitest, moderno e rápido, oferece suporte nativo a essas técnicas através de sua API robusta, tornando-se a escolha ideal para projetos TypeScript e JavaScript contemporâneos. Entendendo os Conceitos Fundamentais Diferenças Práticas Entre Mock, Spy e Stub Um stub substitui uma função real por uma versão simulada que retorna dados pré-configurados, sem registrar chamadas. Um spy envolve uma função existente, permitindo monitorar como ela foi chamada enquanto mantém seu comportamento original. Um mock é uma versão completamente controlada que define comportamentos esperados e valida se foram chamados corretamente. Na prática, considere uma função que busca dados de

Introdução: Por que Mocks, Spies e Stubs são Essenciais

Testes unitários avançados vão além de simples assertions. Quando você testa uma função que depende de APIs externas, bancos de dados ou módulos complexos, precisa de técnicas sofisticadas para isolar o comportamento do código sob teste. Mocks, spies e stubs são as ferramentas que permitem controlar dependências, simular comportamentos e validar interações entre componentes. O Vitest, moderno e rápido, oferece suporte nativo a essas técnicas através de sua API robusta, tornando-se a escolha ideal para projetos TypeScript e JavaScript contemporâneos.

Entendendo os Conceitos Fundamentais

Diferenças Práticas Entre Mock, Spy e Stub

Um stub substitui uma função real por uma versão simulada que retorna dados pré-configurados, sem registrar chamadas. Um spy envolve uma função existente, permitindo monitorar como ela foi chamada enquanto mantém seu comportamento original. Um mock é uma versão completamente controlada que define comportamentos esperados e valida se foram chamados corretamente.

Na prática, considere uma função que busca dados de um servidor:

// Função sob teste
async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

// Teste com stub (retorna dados fixos)
import { describe, it, expect, vi } from 'vitest';

describe('fetchUserData com Stub', () => {
  it('deve processar dados do usuário corretamente', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({ id: 1, name: 'João' })
    });

    const result = await fetchUserData(1);
    expect(result.name).toBe('João');
  });
});

Aqui, fetch foi substituído (stub) por uma versão que retorna sempre o mesmo resultado, isolando a função de teste da rede real.

Quando Usar Cada Técnica

Use stubs quando precisa apenas de valores de retorno previsíveis e não se importa com chamadas. Use spies quando a função original importa, mas você quer monitorar seu comportamento. Use mocks quando precisa validar que certas funções foram chamadas com argumentos específicos e em sequência correta. A escolha depende do que você está testando: lógica (stub), integração superficial (spy), ou contrato entre módulos (mock).

Técnicas Avançadas com Vitest

Spies: Monitorando Comportamento Real

Spies permitem verificar como uma função foi utilizada mantendo sua implementação. Imagine um serviço de notificação:

class NotificationService {
  send(message) {
    console.log(`Email enviado: ${message}`);
    return true;
  }
}

const service = new NotificationService();
const spy = vi.spyOn(service, 'send');

service.send('Bem-vindo!');
service.send('Confirmação');

expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenNthCalledWith(1, 'Bem-vindo!');
expect(spy).toHaveBeenNthCalledWith(2, 'Confirmação');

spy.mockRestore();

O spy registra todas as chamadas e permite assertions detalhadas sobre argumentos e quantidade de invocações. Note que mockRestore() remove a instrumentação, importante para evitar efeitos colaterais entre testes.

Mocks: Validando Contratos

Mocks são ideais para verificar se uma dependência foi usada corretamente. Considere um repositório que precisa comunicar com um banco de dados:

class UserRepository {
  constructor(database) {
    this.db = database;
  }

  createUser(user) {
    return this.db.insert('users', user);
  }

  findById(id) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

describe('UserRepository', () => {
  it('deve inserir usuário corretamente', () => {
    const mockDb = {
      insert: vi.fn().mockResolvedValue({ id: 1 })
    };

    const repo = new UserRepository(mockDb);
    repo.createUser({ name: 'Maria' });

    expect(mockDb.insert).toHaveBeenCalledWith('users', { name: 'Maria' });
  });

  it('deve executar query com parâmetros corretos', () => {
    const mockDb = {
      query: vi.fn().mockResolvedValue([{ id: 1, name: 'Maria' }])
    };

    const repo = new UserRepository(mockDb);
    repo.findById(1);

    expect(mockDb.query).toHaveBeenCalledWith(
      'SELECT * FROM users WHERE id = ?',
      [1]
    );
  });
});

Este padrão isola completamente a camada de dados, permitindo testar a lógica de negócio sem um banco de dados real.

Padrão Avançado: Simulando Erros

Um cenário comum é validar comportamento quando dependências falham:

class PaymentProcessor {
  constructor(gateway) {
    this.gateway = gateway;
  }

  async processPayment(amount) {
    try {
      const result = await this.gateway.charge(amount);
      return { success: true, transactionId: result.id };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
}

describe('PaymentProcessor', () => {
  it('deve tratar falha de gateway graciosamente', async () => {
    const mockGateway = {
      charge: vi.fn().mockRejectedValue(
        new Error('Gateway indisponível')
      )
    };

    const processor = new PaymentProcessor(mockGateway);
    const result = await processor.processPayment(100);

    expect(result.success).toBe(false);
    expect(result.error).toBe('Gateway indisponível');
  });
});

Usando mockRejectedValue, simulamos exceções que a dependência pode lançar, validando se a função sob teste as trata corretamente.

Padrões de Uso e Boas Práticas

Limpeza e Isolamento

Cada teste deve ser independente. Use beforeEach e afterEach para gerenciar estado:

describe('API Service', () => {
  let mockApi;

  beforeEach(() => {
    mockApi = {
      get: vi.fn(),
      post: vi.fn()
    };
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  it('teste 1', () => {
    mockApi.get.mockResolvedValue({ data: 'test' });
    expect(mockApi.get).toHaveBeenCalled();
  });

  it('teste 2', () => {
    // mockApi está limpo aqui
    expect(mockApi.get).not.toHaveBeenCalled();
  });
});

vi.clearAllMocks() reseta o histórico de chamadas, evitando contaminação entre testes.

Evitar Over-Mocking

Não mocke tudo. Se uma função é simples e sem dependências externas, teste-a de verdade. Mocke apenas dependências que causam efeitos colaterais (I/O, rede, tempo):

// ✗ Ruim: mocka lógica pura
const add = (a, b) => a + b;
const mockAdd = vi.fn().mockReturnValue(5);

// ✓ Bom: testa diretamente
expect(add(2, 3)).toBe(5);

Conclusão

Dominar mocks, spies e stubs transforma você em um testador profissional. O Vitest oferece APIs claras e poderosas para cada cenário. Lembre-se: spies monitoram comportamento real, stubs substituem com valores fixos, mocks validam contratos entre componentes. Use cada um em seu contexto apropriado, sempre mantendo testes rápidos, isolados e legíveis. A prática leva à excelência — comece a experimentar essas técnicas em seu próximo projeto.

Referências


Artigos relacionados