MSW em React: Mock Service Worker para Testes e Desenvolvimento na Prática Já leu

O que é MSW (Mock Service Worker)? Mock Service Worker é uma biblioteca JavaScript que intercepta requisições HTTP em nível de rede, permitindo que você simule respostas de APIs sem modificar o código da sua aplicação. Diferente de outras abordagens, o MSW funciona com Service Workers no navegador e com adapters para Node.js, oferecendo uma solução agnóstica de framework. A grande vantagem do MSW é que ele não mocka bibliotecas HTTP específicas — como axios ou fetch — mas sim intercepta as requisições no ponto mais baixo possível. Isso significa que qualquer código que faça uma chamada HTTP funcionará com seus mocks, tornando os testes mais próximos do comportamento real da aplicação. Instalação e Configuração Inicial Para começar, você precisa instalar o MSW e gerar os arquivos necessários: O comando cria um arquivo Service Worker no diretório da sua aplicação. Este arquivo é essencial para interceptar requisições no navegador durante o desenvolvimento e testes. Estrutura de um Handler MSW Um

O que é MSW (Mock Service Worker)?

Mock Service Worker é uma biblioteca JavaScript que intercepta requisições HTTP em nível de rede, permitindo que você simule respostas de APIs sem modificar o código da sua aplicação. Diferente de outras abordagens, o MSW funciona com Service Workers no navegador e com adapters para Node.js, oferecendo uma solução agnóstica de framework.

A grande vantagem do MSW é que ele não mocka bibliotecas HTTP específicas — como axios ou fetch — mas sim intercepta as requisições no ponto mais baixo possível. Isso significa que qualquer código que faça uma chamada HTTP funcionará com seus mocks, tornando os testes mais próximos do comportamento real da aplicação.

Instalação e Configuração Inicial

Para começar, você precisa instalar o MSW e gerar os arquivos necessários:

npm install msw --save-dev
npx msw init public/

O comando msw init cria um arquivo Service Worker no diretório public/ da sua aplicação. Este arquivo é essencial para interceptar requisições no navegador durante o desenvolvimento e testes.

Estrutura de um Handler MSW

Um handler no MSW define qual requisição será interceptada e qual será a resposta. Vamos criar nosso primeiro arquivo de handlers:

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

export const handlers = [
  // GET com resposta bem-sucedida
  http.get('/api/users', () => {
    return HttpResponse.json(
      [
        { id: 1, name: 'João Silva', email: 'joao@example.com' },
        { id: 2, name: 'Maria Santos', email: 'maria@example.com' }
      ],
      { status: 200 }
    );
  }),

  // POST com validação
  http.post('/api/users', async ({ request }) => {
    const body = await request.json();

    if (!body.name || !body.email) {
      return HttpResponse.json(
        { error: 'Nome e email são obrigatórios' },
        { status: 400 }
      );
    }

    return HttpResponse.json(
      { id: 3, ...body, createdAt: new Date().toISOString() },
      { status: 201 }
    );
  }),

  // Erro simulado
  http.get('/api/profile/:id', ({ params }) => {
    if (params.id === '999') {
      return HttpResponse.json(
        { error: 'Usuário não encontrado' },
        { status: 404 }
      );
    }

    return HttpResponse.json({
      id: params.id,
      name: 'João Silva',
      role: 'Developer'
    });
  })
];

Configurando o Servidor MSW para Testes

Para usar MSW nos testes com Jest ou Vitest, crie um arquivo de configuração:

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

export const server = setupServer(...handlers);

Em seguida, configure seu arquivo de setup dos testes:

// src/setupTests.js
import { server } from './mocks/server';

// Inicia o servidor antes de todos os testes
beforeAll(() => server.listen());

// Reseta os handlers após cada teste
afterEach(() => server.resetHandlers());

// Encerra o servidor após todos os testes
afterAll(() => server.close());

Testes com React e MSW

Testando Componentes que Fazem Requisições

Vamos criar um componente React que busca dados e testá-lo com MSW:

// src/components/UserList.jsx
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Carregando...</div>;
  if (error) return <div>Erro: {error}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name} - {user.email}</li>
      ))}
    </ul>
  );
}

Agora o teste:

// src/components/UserList.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserList } from './UserList';
import { server } from '../mocks/server';
import { http, HttpResponse } from 'msw';

describe('UserList', () => {
  test('renderiza lista de usuários com sucesso', async () => {
    render(<UserList />);

    expect(screen.getByText('Carregando...')).toBeInTheDocument();

    await waitFor(() => {
      expect(screen.getByText('João Silva - joao@example.com')).toBeInTheDocument();
      expect(screen.getByText('Maria Santos - maria@example.com')).toBeInTheDocument();
    });
  });

  test('exibe mensagem de erro quando a API falha', async () => {
    // Sobrescreve o handler para este teste específico
    server.use(
      http.get('/api/users', () => {
        return HttpResponse.json(
          { error: 'Erro interno do servidor' },
          { status: 500 }
        );
      })
    );

    render(<UserList />);

    await waitFor(() => {
      expect(screen.getByText(/Erro:/i)).toBeInTheDocument();
    });
  });
});

Testando Formulários com Requisições POST

// src/components/CreateUser.jsx
import { useState } from 'react';

export function CreateUser({ onUserCreated }) {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      const res = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name, email })
      });

      if (!res.ok) {
        const data = await res.json();
        throw new Error(data.error);
      }

      const newUser = await res.json();
      onUserCreated(newUser);
      setName('');
      setEmail('');
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Nome"
        required
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Criando...' : 'Criar Usuário'}
      </button>
      {error && <span className="error">{error}</span>}
    </form>
  );
}

Teste correspondente:

// src/components/CreateUser.test.jsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { CreateUser } from './CreateUser';
import { server } from '../mocks/server';

describe('CreateUser', () => {
  test('cria um usuário com sucesso', async () => {
    const mockCallback = jest.fn();
    render(<CreateUser onUserCreated={mockCallback} />);

    fireEvent.change(screen.getByPlaceholderText('Nome'), {
      target: { value: 'Pedro Costa' }
    });
    fireEvent.change(screen.getByPlaceholderText('Email'), {
      target: { value: 'pedro@example.com' }
    });

    fireEvent.click(screen.getByRole('button', { name: /Criar Usuário/i }));

    await waitFor(() => {
      expect(mockCallback).toHaveBeenCalledWith(
        expect.objectContaining({
          name: 'Pedro Costa',
          email: 'pedro@example.com'
        })
      );
    });
  });

  test('exibe erro de validação quando faltam campos', async () => {
    const mockCallback = jest.fn();
    render(<CreateUser onUserCreated={mockCallback} />);

    fireEvent.change(screen.getByPlaceholderText('Nome'), {
      target: { value: 'Pedro Costa' }
    });

    fireEvent.click(screen.getByRole('button'));

    await waitFor(() => {
      expect(screen.getByText(/obrigatórios/i)).toBeInTheDocument();
      expect(mockCallback).not.toHaveBeenCalled();
    });
  });
});

Desenvolvimento Local com MSW

Usando MSW no Navegador Durante o Desenvolvimento

Para usar MSW enquanto desenvolve localmente, adicione o seguinte ao seu arquivo principal:

// src/main.jsx (Vite) ou src/index.js (Create React App)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

async function enableMocking() {
  if (process.env.NODE_ENV === 'development') {
    const { worker } = await import('./mocks/browser');
    return worker.start();
  }
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
});

Crie um arquivo para exportar o worker do navegador:

// src/mocks/browser.js
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);

Agora, quando você iniciar a aplicação, o MSW interceptará todas as requisições, permitindo que você trabalhe sem uma API real.

Simulando Comportamentos Realistas

Para testes mais robustos, simule delays de rede e cenários realistas:

// src/mocks/handlers.js (adição)
import { http, HttpResponse, delay } from 'msw';

export const handlersWithDelay = [
  http.get('/api/users', async () => {
    await delay(800); // Simula latência de rede
    return HttpResponse.json([
      { id: 1, name: 'João Silva', email: 'joao@example.com' }
    ]);
  }),

  http.post('/api/payment', async ({ request }) => {
    await delay(2000); // Simula processamento lento
    const body = await request.json();

    if (body.cardToken === 'INVALID') {
      return HttpResponse.json(
        { error: 'Cartão inválido' },
        { status: 400 }
      );
    }

    return HttpResponse.json({
      transactionId: Math.random().toString(36).substring(7),
      status: 'approved',
      amount: body.amount
    });
  })
];

Conclusão

Você aprendeu que MSW é uma solução poderosa e agnóstica que intercepta requisições em nível de rede, funcionando tanto no navegador quanto em testes Node.js. Isso torna seus testes mais robustos e realistas, já que o código da sua aplicação não precisa saber que está fazendo requisições mockadas.

A segunda lição importante é que MSW permite sobrescrever handlers por teste, oferecendo granularidade máxima para simular diferentes cenários sem contaminar outros testes. Você viu como isso funciona ao usar server.use() para definir comportamentos específicos.

Por fim, entenda que MSW reduz a complexidade de desenvolvimento e testes, eliminando a necessidade de manter fixtures de dados em múltiplos lugares ou modificar sua lógica de requisições. Você trabalha com uma API consistente que funciona tanto durante o desenvolvimento quanto nos testes automatizados.

Referências


Artigos relacionados