O Mock Service Worker no Node.js, conhecido como MSW, intercepta requisições HTTP feitas pelo processo e retorna respostas controladas durante testes e desenvolvimento. Em vez de substituir manualmente cada função de cliente, o teste descreve o comportamento da API externa no nível da rede.
Essa abordagem permite reutilizar handlers entre testes Node.js, navegador e demos. O código da aplicação continua usando fetch, Axios, GraphQL ou bibliotecas reais, enquanto MSW controla a resposta. Porém, mocks não substituem testes contra serviços reais e precisam ser resetados para evitar vazamento de estado entre casos.
Neste guia, você aprenderá a instalar MSW, criar handlers REST e GraphQL, integrar com Node Test Runner e Vitest, simular erros, delays, uploads e cenários dinâmicos, além de manter mocks alinhados ao contrato.
Como MSW funciona no Node.js?
A documentação oficial da integração Node.js do MSW explica que a biblioteca intercepta módulos responsáveis por requisições, incluindo mecanismos nativos usados por http, https e fetch.
Ela não sobe um servidor HTTP separado. O interceptador atua apenas no processo atual.
Instalação
npm install --save-dev mswMantenha MSW em devDependencies e fixe a versão pelo lockfile.
Primeiro handler
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('https://payments.example.com/status', () => {
return HttpResponse.json({
status: 'available'
});
})
];Configurando setupServer
import { setupServer } from 'msw/node';
import { handlers } from './handlers.js';
export const mockServer = setupServer(...handlers);O nome setupServer pode causar confusão: ele cria um interceptador, não um servidor de rede acessível externamente.
Lifecycle no Node Test Runner
import { before, after, afterEach } from 'node:test';
import { mockServer } from './mocks/server.js';
before(() => {
mockServer.listen({
onUnhandledRequest: 'error'
});
});
afterEach(() => {
mockServer.resetHandlers();
});
after(() => {
mockServer.close();
});Veja Node Test Runner.
Por que resetar handlers?
Um teste pode adicionar comportamento temporário:
mockServer.use(
http.get('https://payments.example.com/status', () => {
return HttpResponse.json(
{ error: 'unavailable' },
{ status: 503 }
);
})
);Sem resetHandlers(), o override pode afetar testes seguintes.
Falhar em requisições não tratadas
mockServer.listen({
onUnhandledRequest: 'error'
});Isso impede que um teste acesse a internet acidentalmente. Em suítes com requests locais reais, use uma função para permitir apenas hosts conhecidos.
Handler com parâmetros
http.get(
'https://catalog.example.com/products/:productId',
({ params }) => {
return HttpResponse.json({
id: params.productId,
name: 'Teclado',
price: 19900
});
}
)Query parameters
http.get('https://catalog.example.com/products', ({ request }) => {
const url = new URL(request.url);
const category = url.searchParams.get('category');
return HttpResponse.json({
data: category === 'hardware'
? [{ id: 'p1', name: 'Teclado' }]
: []
});
});Não inclua query string na URL do handler; leia com URL.
Lendo JSON
http.post('https://payments.example.com/charges', async ({ request }) => {
const body = await request.json();
if (body.amount > 100000) {
return HttpResponse.json(
{ code: 'limit_exceeded' },
{ status: 422 }
);
}
return HttpResponse.json(
{ id: 'charge-1', status: 'approved' },
{ status: 201 }
);
});Headers e autenticação
http.get('https://api.example.com/account', ({ request }) => {
const authorization = request.headers.get('authorization');
if (authorization !== 'Bearer test-token') {
return HttpResponse.json(
{ title: 'Unauthorized', status: 401 },
{
status: 401,
headers: {
'Content-Type': 'application/problem+json'
}
}
);
}
return HttpResponse.json({ id: 'account-1' });
});Consulte Problem Details no Node.js.
Simulando delay
import { delay, http, HttpResponse } from 'msw';
http.get('https://api.example.com/report', async () => {
await delay(500);
return HttpResponse.json({ ready: true });
});Use delays pequenos e explícitos. Testes com tempo real ficam lentos; quando possível, teste timeouts com relógio controlado.
Network error
http.get('https://api.example.com/report', () => {
return HttpResponse.error();
});Esse cenário é diferente de uma resposta HTTP 500. Network error significa que não existe resposta válida.
Resposta 500
http.get('https://api.example.com/report', () => {
return HttpResponse.json(
{ title: 'Internal Server Error', status: 500 },
{ status: 500 }
);
});Teste separadamente 500, timeout, conexão interrompida e JSON inválido.
Retries
Para simular falha seguida de sucesso:
let attempts = 0;
mockServer.use(
http.get('https://api.example.com/data', () => {
attempts += 1;
if (attempts < 3) {
return HttpResponse.json({}, { status: 503 });
}
return HttpResponse.json({ value: 42 });
})
);Resete o contador dentro do teste. Veja Retry com Backoff no Node.js.
Streaming
http.get('https://api.example.com/stream', () => {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk-1\n'));
controller.enqueue(new TextEncoder().encode('chunk-2\n'));
controller.close();
}
});
return new HttpResponse(stream, {
headers: { 'Content-Type': 'text/plain' }
});
});Consulte Web Streams API no Node.js.
Uploads
http.post('https://uploads.example.com/files', async ({ request }) => {
const formData = await request.formData();
const file = formData.get('file');
if (!(file instanceof File)) {
return HttpResponse.json({}, { status: 400 });
}
return HttpResponse.json({
id: 'file-1',
name: file.name,
size: file.size
});
});GraphQL
import { graphql, HttpResponse } from 'msw';
export const handlers = [
graphql.query('GetProduct', ({ variables }) => {
return HttpResponse.json({
data: {
product: {
id: variables.id,
name: 'Teclado'
}
}
});
})
];O nome da operação facilita handlers específicos.
Erros GraphQL
graphql.mutation('CreateOrder', () => {
return HttpResponse.json({
errors: [
{
message: 'Produto indisponível',
extensions: { code: 'OUT_OF_STOCK' }
}
]
});
})Handlers compartilhados
mocks/
├── handlers/
│ ├── catalog.ts
│ ├── payments.ts
│ └── users.ts
├── handlers.ts
├── node.ts
└── browser.tsO mesmo array pode ser usado por setupServer no Node.js e setupWorker no navegador.
Cenários dinâmicos
Prefira overrides por teste:
mockServer.use(paymentDeclinedHandler);Evite um objeto global de estado que vários testes alteram em paralelo.
Handlers por cenário
export const paymentScenarios = {
approved: http.post(url, () =>
HttpResponse.json({ status: 'approved' })),
declined: http.post(url, () =>
HttpResponse.json(
{ code: 'declined' },
{ status: 422 }
))
};Assertions
MSW recomenda focar no resultado da aplicação em vez de transformar handlers em spies. Teste que o serviço retornou o estado esperado, não detalhes internos de cada request.
Quando o contrato da requisição é importante, valide o body dentro do handler e retorne erro claro.
Eventos de lifecycle
mockServer.events.on('request:start', ({ request }) => {
console.log('MSW:', request.method, request.url);
});Use para diagnóstico, não para logs permanentes no CI.
Bypass e passthrough
MSW pode deixar uma requisição real passar. Evite isso em testes unitários, pois a suíte passa a depender da rede. Use apenas em cenários de desenvolvimento controlados.
Contrato com OpenAPI
Handlers escritos manualmente podem ficar desatualizados. Gere tipos ou valide fixtures contra schemas.
Veja OpenAPI com Node.js.
TypeScript
type ChargeRequest = {
amount: number;
currency: 'BRL';
};
type ChargeResponse = {
id: string;
status: 'approved';
};
http.post<never, ChargeRequest, ChargeResponse>(
url,
async ({ request }) => {
const body = await request.json();
return HttpResponse.json({
id: 'charge-1',
status: 'approved'
});
}
)Testes em paralelo
Overrides globais podem colidir entre testes concorrentes. Use isolamento, server.boundary() quando adequado ou evite executar o grupo em paralelo.
Não mockar a própria API
Para testar uma API Node.js, chame a aplicação em memória ou por HTTP local. MSW é mais útil para dependências externas chamadas pela API.
Testcontainers e MSW
Use MSW para serviços HTTP externos e Testcontainers no Node.js para banco, Redis e filas reais.
CI
- run: npm ci
- run: npm run typecheck
- run: npm testCom onUnhandledRequest: error, o CI detecta qualquer acesso de rede não previsto.
Segurança
- Não use tokens reais.
- Não grave payloads sensíveis.
- Não permita requests externos inesperados.
- Use domínios
example.com. - Mantenha fixtures fictícias.
- Fixe a versão do MSW.
- Feche o interceptador após os testes.
Erros comuns
- Não resetar handlers: testes vazam estado.
- Não fechar servidor: processo pode ficar ativo.
- Unhandled como bypass: teste acessa internet.
- Mock diferente do contrato: produção quebra.
- Assertion em detalhes internos: teste fica frágil.
- Override global paralelo: cenários colidem.
- Delay real longo: suíte fica lenta.
- Confundir network error com 500: tratamento errado.
Configuração recomendada
// test/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers.js';
export const mockServer = setupServer(...handlers);
// test/setup.ts
import { before, after, afterEach } from 'node:test';
import { mockServer } from './mocks/server.js';
before(() => {
mockServer.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
mockServer.resetHandlers();
});
after(() => {
mockServer.close();
});Conclusão
O Mock Service Worker no Node.js intercepta HTTP no nível da rede e permite testar integrações sem alterar o cliente da aplicação. Handlers podem ser compartilhados com navegador, demos e diferentes test runners.
Falhe em requests não tratados, resete overrides e valide contratos. Com MSW para serviços HTTP e Testcontainers para infraestrutura, testes ganham realismo sem depender de ambientes externos instáveis.


