Mocking de API em testes frontend

1. Por que mockar APIs em testes frontend?

Testar componentes React que consomem APIs externas apresenta desafios significativos. Dependências de rede tornam os testes lentos, instáveis e difíceis de reproduzir. Quando um teste falha, você nunca tem certeza se o problema está no seu código ou no servidor remoto.

Mockar APIs resolve esses problemas ao:
- Isolar o frontend de servidores, bancos de dados e serviços externos
- Garantir velocidade e confiabilidade — testes que dependem de rede podem levar segundos e falhar intermitentemente
- Simular cenários extremos que seriam difíceis de reproduzir com APIs reais: erros 500, timeouts, respostas lentas, payloads malformados

// Exemplo de teste sem mock — frágil e lento
test('carrega usuários da API real', async () => {
  const response = await fetch('https://api.exemplo.com/users');
  const data = await response.json();
  expect(data.length).toBeGreaterThan(0); // Falha se API estiver fora do ar
});

2. Abordagens de mocking: stub, spy e fake

Existem três estratégias principais para simular dependências externas:

Stub: Substitui uma função por uma versão que retorna respostas fixas e previsíveis.

// Stub simples com Jest
jest.spyOn(global, 'fetch').mockResolvedValue({
  json: async () => ({ id: 1, name: 'João' })
});

Spy: Monitora chamadas e argumentos sem necessariamente alterar o comportamento real.

const fetchSpy = jest.spyOn(window, 'fetch');
render(<MeuComponente />);
expect(fetchSpy).toHaveBeenCalledWith('/api/users');

Fake: Implementação simplificada de um serviço completo, como um servidor HTTP local.

3. Mocking no nível de fetch/axios com bibliotecas

MSW (Mock Service Worker)

O MSW intercepta requisições reais usando Service Worker no navegador ou por interceptação de rede no Node.js. É a abordagem mais próxima de um teste realista.

// handlers.js
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Ana' },
      { id: 2, name: 'Carlos' }
    ]);
  }),

  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({ id: Number(params.id), name: 'Usuário' });
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: 3, ...body }, { status: 201 });
  })
];

Nock

Ideal para testes de integração no Node.js, intercepta requisições HTTP reais.

const nock = require('nock');

nock('https://api.exemplo.com')
  .get('/users')
  .reply(200, [{ id: 1, name: 'Maria' }]);

Jest mocks manuais

Útil para mocking rápido de módulos específicos.

jest.mock('../services/api', () => ({
  fetchUsers: jest.fn().mockResolvedValue([{ id: 1, name: 'Pedro' }])
}));

4. Prática com React Testing Library + MSW

Configuração do servidor MSW

// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
// src/setupTests.js
import { server } from './mocks/server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Testando componentes com diferentes estados

// UserList.jsx
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/users')
      .then(res => {
        if (!res.ok) throw new Error('Erro na requisição');
        return res.json();
      })
      .then(data => { setUsers(data); setLoading(false); })
      .catch(err => { setError(err.message); setLoading(false); });
  }, []);

  if (loading) return <div data-testid="loading">Carregando...</div>;
  if (error) return <div data-testid="error">{error}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id} data-testid="user-item">{user.name}</li>
      ))}
    </ul>
  );
}
// UserList.test.js
import { render, screen, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { server } from './mocks/server';
import UserList from './UserList';

test('exibe lista de usuários após carregamento', async () => {
  render(<UserList />);

  expect(screen.getByTestId('loading')).toBeInTheDocument();

  await waitFor(() => {
    expect(screen.getByText('Ana')).toBeInTheDocument();
    expect(screen.getByText('Carlos')).toBeInTheDocument();
  });
});

test('exibe mensagem de erro quando API falha', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ message: 'Internal Server Error' }, { status: 500 });
    })
  );

  render(<UserList />);

  await waitFor(() => {
    expect(screen.getByTestId('error')).toHaveTextContent('Erro na requisição');
  });
});

5. Testando estados de UI com mocks

Simulação de carregamento com delay

test('mostra loading por tempo suficiente', async () => {
  server.use(
    http.get('/api/users', async () => {
      await new Promise(resolve => setTimeout(resolve, 2000));
      return HttpResponse.json([{ id: 1, name: 'Lento' }]);
    })
  );

  render(<UserList />);
  expect(screen.getByTestId('loading')).toBeInTheDocument();
});

Simulação de diferentes erros HTTP

const errorScenarios = [
  { status: 401, message: 'Não autorizado' },
  { status: 404, message: 'Não encontrado' },
  { status: 500, message: 'Erro interno' }
];

errorScenarios.forEach(({ status, message }) => {
  test(`trata erro ${status}`, async () => {
    server.use(
      http.get('/api/users', () => {
        return HttpResponse.json({ message }, { status });
      })
    );

    render(<UserList />);
    await waitFor(() => {
      expect(screen.getByTestId('error')).toBeInTheDocument();
    });
  });
});

Resposta vazia ou parcial

test('lida com lista vazia', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json([]);
    })
  );

  render(<UserList />);
  await waitFor(() => {
    expect(screen.queryByTestId('user-item')).not.toBeInTheDocument();
  });
});

6. Boas práticas e armadilhas comuns

Evite mocks frágeis

Não acople seus testes a detalhes internos da implementação. Teste o comportamento observável pelo usuário, não chamadas de função específicas.

// ❌ Ruim: testa detalhe interno
expect(fetchMock).toHaveBeenCalledWith('/api/users', { method: 'GET' });

// ✅ Bom: testa comportamento visível
expect(screen.getByText('Ana')).toBeInTheDocument();

Mantenha mocks atualizados

Use contratos de API (OpenAPI/Swagger) para gerar automaticamente handlers do MSW e evitar divergências.

Limpeza entre testes

afterEach(() => {
  server.resetHandlers(); // Remove handlers personalizados
  jest.clearAllMocks();   // Limpa spies e stubs
});

7. Mocking em testes de integração vs. E2E

Tipo de Teste O que mockar Quando usar
Unitário Tudo externo Testar lógica isolada do componente
Integração API externa, mas testa fluxo interno Validar interação entre componentes
E2E Nada (API real) Validar fluxo completo do usuário

Estratégia híbrida: Use mocks para testes de unidade/integração (rápidos e controlados) e complemente com testes E2E em ambiente de staging para cenários críticos.

8. Ferramentas complementares e próximos passos

  • json-server: Crie uma API REST fake em segundos para desenvolvimento local
  • Faker.js: Gere dados realistas (nomes, emails, endereços) para seus mocks
  • Cypress: Oferece interceptação de rede nativa para testes E2E com mocks
// Exemplo de integração com CI/CD
// .github/workflows/test.yml
name: Testes
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm test -- --coverage  # MSW roda em background automaticamente

O mocking de API é uma habilidade essencial para construir testes frontend robustos e confiáveis. Com ferramentas como MSW e React Testing Library, você pode simular qualquer cenário de forma previsível e rápida, garantindo que seu componente se comporte corretamente em todas as situações.

Referências