msw для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

msw для TypeScript: полное руководство с примерами

Что это и зачем нужна?

Mock Service Worker (msw) — это библиотека для перехвата и мокирования сетевых запросов на уровне Service Worker API. В отличие от традиционных решений (например, axios-mock-adapter или nock), msw работает на уровне браузера или Node.js, перехватывая реальные HTTP-запросы и возвращая заранее заданные ответы. Это позволяет тестировать приложения без необходимости запускать реальный сервер или изменять код приложения.

Основная цель msw — предоставить единый инструмент для мокирования API как в браузерных тестах (с использованием реального Service Worker), так и в серверных (Node.js). Библиотека особенно полезна для разработчиков, использующих TypeScript, так как предоставляет строгую типизацию для всех запросов и ответов. msw идеально подходит для юнит-тестов, интеграционных тестов, а также для разработки фронтенда в изоляции от бэкенда.

Ключевое преимущество msw — это то, что моки работают на уровне сети, а не на уровне кода. Это означает, что любые библиотеки для выполнения HTTP-запросов (fetch, axios, graphql-request и т.д.) будут автоматически перехвачены без дополнительной настройки. Библиотека поддерживает REST API, GraphQL, а также позволяет обрабатывать WebSocket соединения (через расширения).

Установка

Для установки msw в проект на TypeScript выполните одну из следующих команд:

# npm
npm install msw --save-dev

# yarn
yarn add msw --dev

# pnpm
pnpm add msw --save-dev

После установки необходимо инициализировать Service Worker для браузерного использования:

npx msw init public/ --save

Для серверного использования (Node.js) дополнительная инициализация не требуется.

Быстрый старт — минимальный рабочий пример

Создадим простой мок для REST API. Пример включает определение обработчика, настройку сервера и тестовый запрос.

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

export const handlers = [
  http.get('https://api.example.com/user', () => {
    return HttpResponse.json({
      id: 'c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3f',
      firstName: 'John',
      lastName: 'Maverick',
    });
  }),
];

// server.ts (для Node.js)
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

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

// test.ts
import { server } from './server';

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

test('should fetch user', async () => {
  const response = await fetch('https://api.example.com/user');
  const data = await response.json();
  expect(data.firstName).toBe('John');
});

Основные методы и функции

1. http.get()

Сигнатура: http.get(url: string | RegExp, resolver: (info: HttpRequestInfo) => HttpResponse)

Описание: Создает обработчик для GET-запроса по указанному URL. Поддерживает точные строки, регулярные выражения и шаблоны с параметрами.

import { http, HttpResponse } from 'msw';

export const handler = http.get('https://api.example.com/users/:id', ({ params }) => {
  const { id } = params;
  return HttpResponse.json({ id, name: 'User ' + id });
});

2. http.post()

Сигнатура: http.post(url: string | RegExp, resolver: (info: HttpRequestInfo) => HttpResponse)

Описание: Создает обработчик для POST-запроса. Позволяет читать тело запроса через request.json().

import { http, HttpResponse } from 'msw';

export const handler = http.post('https://api.example.com/users', async ({ request }) => {
  const body = await request.json();
  return HttpResponse.json({ id: 'new-id', ...body }, { status: 201 });
});

3. http.all()

Сигнатура: http.all(url: string | RegExp, resolver: (info: HttpRequestInfo) => HttpResponse)

Описание: Создает обработчик для любого HTTP-метода (GET, POST, PUT, DELETE и т.д.) по указанному URL.

import { http, HttpResponse } from 'msw';

export const handler = http.all('https://api.example.com/*', ({ request }) => {
  return HttpResponse.json({ method: request.method, url: request.url });
});

4. HttpResponse.json()

Сигнатура: HttpResponse.json(body: any, init?: ResponseInit)

Описание: Создает HTTP-ответ с JSON-телом. Вторым аргументом можно передать статус, заголовки и другие параметры.

import { HttpResponse } from 'msw';

const response = HttpResponse.json(
  { message: 'Success' },
  { status: 200, headers: { 'X-Custom': 'value' } }
);

5. HttpResponse.text()

Сигнатура: HttpResponse.text(body: string, init?: ResponseInit)

Описание: Создает HTTP-ответ с текстовым телом. Полезно для мокирования HTML, CSV или plain text ответов.

import { HttpResponse } from 'msw';

const response = HttpResponse.text('Hello, World!', {
  status: 200,
  headers: { 'Content-Type': 'text/plain' },
});

6. HttpResponse.xml()

Сигнатура: HttpResponse.xml(body: string, init?: ResponseInit)

Описание: Создает HTTP-ответ с XML-телом. Автоматически устанавливает заголовок Content-Type в application/xml.

import { HttpResponse } from 'msw';

const response = HttpResponse.xml('<root><item>value</item></root>');

7. HttpResponse.arrayBuffer()

Сигнатура: HttpResponse.arrayBuffer(body: ArrayBuffer, init?: ResponseInit)

Описание: Создает HTTP-ответ с бинарными данными (ArrayBuffer). Используется для мокирования изображений, файлов и других бинарных ресурсов.

import { HttpResponse } from 'msw';

const buffer = new ArrayBuffer(8);
const response = HttpResponse.arrayBuffer(buffer, {
  headers: { 'Content-Type': 'application/octet-stream' },
});

8. delay()

Сигнатура: delay(ms: number): Promise<void>

Описание: Вспомогательная функция для добавления искусственной задержки в обработчике. Полезна для симуляции сетевых задержек.

import { http, HttpResponse, delay } from 'msw';

export const handler = http.get('https://api.example.com/slow', async () => {
  await delay(2000); // 2 секунды задержки
  return HttpResponse.json({ status: 'ok' });
});

9. setupServer()

Сигнатура: setupServer(...handlers: HttpHandler[]): SetupServerApi

Описание: Создает сервер для мокирования в среде Node.js. Возвращает объект с методами listen, close, resetHandlers и use.

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

const server = setupServer(...handlers);

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

10. setupWorker()

Сигнатура: setupWorker(...handlers: HttpHandler[]): SetupWorkerApi

Описание: Создает Service Worker для мокирования в браузере. Используется в development-режиме или в браузерных тестах.

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

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

11. bypass()

Сигнатура: bypass(request: Request): Promise<Response>

Описание: Позволяет пропустить запрос через мок и выполнить реальный сетевой запрос. Используется внутри обработчика для частичного мокирования.

import { http, HttpResponse, bypass } from 'msw';

export const handler = http.get('https://api.example.com/proxy', async ({ request }) => {
  if (request.headers.get('x-mock') === 'true') {
    return HttpResponse.json({ mocked: true });
  }
  const realResponse = await bypass(request);
  return realResponse;
});

Реальный практический пример

Рассмотрим полноценный пример тестирования приложения для управления задачами (Todo App) с использованием msw и TypeScript. Приложение взаимодействует с REST API для CRUD-операций.

// types.ts
export interface Todo {
  id: string;
  title: string;
  completed: boolean;
  createdAt: string;
}

// api.ts
export class TodoApi {
  private baseUrl = 'https://api.example.com/todos';

  async getAll(): Promise<Todo[]> {
    const response = await fetch(this.baseUrl);
    if (!response.ok) throw new Error('Failed to fetch todos');
    return response.json();
  }

  async create(title: string): Promise<Todo> {
    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, completed: false }),
    });
    if (!response.ok) throw new Error('Failed to create todo');
    return response.json();
  }

  async update(id: string, data: Partial<Todo>): Promise<Todo> {
    const response = await fetch(`${this.baseUrl}/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    if (!response.ok) throw new Error('Failed to update todo');
    return response.json();
  }

  async delete(id: string): Promise<void> {
    const response = await fetch(`${this.baseUrl}/${id}`, { method: 'DELETE' });
    if (!response.ok) throw new Error('Failed to delete todo');
  }
}

// handlers.ts
import { http, HttpResponse, delay } from 'msw';
import { Todo } from './types';

const todos: Todo[] = [
  { id: '1', title: 'Learn MSW', completed: false, createdAt: '2024-01-01' },
  { id: '2', title: 'Write tests', completed: true, createdAt: '2024-01-02' },
];

export const handlers = [
  http.get('https://api.example.com/todos', async () => {
    await delay(100);
    return HttpResponse.json(todos);
  }),

  http.post('https://api.example.com/todos', async ({ request }) => {
    await delay(150);
    const body = await request.json() as { title: string };
    const newTodo: Todo = {
      id: String(Date.now()),
      title: body.title,
      completed: false,
      createdAt: new Date().toISOString(),
    };
    todos.push(newTodo);
    return HttpResponse.json(newTodo, { status: 201 });
  }),

  http.patch('https://api.example.com/todos/:id', async ({ params, request }) => {
    await delay(100);
    const { id } = params;
    const body = await request.json() as Partial<Todo>;
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) {
      return HttpResponse.json({ error: 'Todo not found' }, { status: 404 });
    }
    todos[index] = { ...todos[index], ...body };
    return HttpResponse.json(todos[index]);
  }),

  http.delete('https://api.example.com/todos/:id', async ({ params }) => {
    await delay(100);
    const { id } = params;
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) {
      return HttpResponse.json({ error: 'Todo not found' }, { status: 404 });
    }
    todos.splice(index, 1);
    return HttpResponse.json(null, { status: 204 });
  }),
];

// server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

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

// todo.test.ts
import { server } from './server';
import { TodoApi } from './api';

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

describe('TodoApi', () => {
  const api = new TodoApi();

  test('should fetch all todos', async () => {
    const todos = await api.getAll();
    expect(todos).toHaveLength(2);
    expect(todos[0].title).toBe('Learn MSW');
  });

  test('should create a new todo', async () => {
    const newTodo = await api.create('Test MSW');
    expect(newTodo.title).toBe('Test MSW');
    expect(newTodo.completed).toBe(false);
    expect(newTodo.id).toBeDefined();
  });

  test('should update a todo', async () => {
    const updated = await api.update('1', { completed: true });
    expect(updated.completed).toBe(true);
  });

  test('should delete a todo', async () => {
    await api.delete('2');
    const todos = await api.getAll();
    expect(todos).toHaveLength(2); // один остался от create, один изначальный
  });

  test('should handle 404 on update', async () => {
    await expect(api.update('999', { title: 'Ghost' })).rejects.toThrow('Failed to update todo');
  });

  test('should handle network error', async () => {
    server.use(
      http.get('https://api.example.com/todos', () => {
        return HttpResponse.error();
      })
    );
    await expect(api.getAll()).rejects.toThrow('Failed to fetch todos');
  });
});

Советы по производительности и типичные ошибки

Советы по производительности

  • Используйте server.listen() один раз: В тестах вызывайте

Рекомендаций