nock для TypeScript: HTTP-мокирование в тестах
1. Что такое nock и зачем она нужна?
nock — это мощная библиотека для Node.js, предназначенная для перехвата и мокирования HTTP-запросов. Она позволяет разработчикам имитировать ответы от внешних сервисов (REST API, сторонние микросервисы, вебхуки) без необходимости реального выполнения сетевых запросов. Это особенно важно при написании юнит-тестов и интеграционных тестов, где стабильность и скорость выполнения имеют критическое значение.
Зачем это нужно? Представьте, что ваше приложение отправляет запросы к платежному шлюзу, погодному API или базе данных через HTTP. В тестовой среде вы не хотите зависеть от доступности этих сервисов, их скорости ответа или лимитов запросов. nock позволяет "подменить" эти запросы, возвращая заранее заданные данные. Это делает тесты быстрыми, предсказуемыми и изолированными от внешних факторов. Кроме того, вы можете тестировать сценарии ошибок (таймауты, коды 500, пустые ответы), которые сложно воспроизвести в реальных условиях.
В контексте TypeScript nock отлично интегрируется с популярными тестовыми фреймворками (Jest, Mocha, Vitest) и поддерживает типизацию, что делает работу с ней удобной и безопасной. Библиотека перехватывает запросы на уровне Node.js (через модуль http и https), поэтому она работает с любыми HTTP-клиентами: axios, node-fetch, got и даже нативным fetch (с некоторыми оговорками).
2. Установка
Для установки nock в проект на TypeScript выполните одну из следующих команд в терминале:
# Через npm
npm install --save-dev nock @types/nock
# Через yarn
yarn add --dev nock @types/nock
# Через pnpm
pnpm add --save-dev nock @types/nock
Важно: @types/nock содержит декларации типов для TypeScript. Если вы используете TypeScript 4.0+, типы могут быть уже встроены в саму библиотеку, но для совместимости лучше установить их явно.
3. Быстрый старт
Давайте напишем минимальный тест, который демонстрирует базовую работу nock. Предположим, у нас есть функция, которая получает данные пользователя с внешнего API.
// userService.ts
import axios from 'axios';
export async function getUser(userId: number): Promise<{ id: number; name: string; email: string }> {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
return response.data;
}
Теперь напишем тест, который мокирует этот запрос:
// userService.test.ts
import nock from 'nock';
import { getUser } from './userService';
describe('getUser', () => {
afterEach(() => {
nock.cleanAll(); // Очищаем все моки после каждого теста
});
it('должен вернуть данные пользователя', async () => {
// 1. Настраиваем перехват
const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com' };
nock('https://jsonplaceholder.typicode.com')
.get('/users/1')
.reply(200, mockUser);
// 2. Вызываем тестируемую функцию
const user = await getUser(1);
// 3. Проверяем результат
expect(user).toEqual(mockUser);
});
it('должен выбросить ошибку при 500', async () => {
nock('https://jsonplaceholder.typicode.com')
.get('/users/1')
.reply(500, { error: 'Server Error' });
await expect(getUser(1)).rejects.toThrow();
});
});
Этот пример показывает, как легко можно подменить ответ сервера и протестировать как успешные, так и ошибочные сценарии.
4. Основные методы и функции
Рассмотрим ключевые методы nock. Каждый метод будет представлен с сигнатурой, описанием и примером.
4.1. nock(baseUrl: string, options?: NockOptions)
Сигнатура: nock(baseUrl: string, options?: { allowUnmocked?: boolean; reqheaders?: Record<string, string>; ... }): Scope
Что делает: Создает новый перехватчик (scope) для указанного базового URL. Все последующие методы (get, post, reply) будут применяться к этому scope. Это отправная точка для любого мокирования.
import nock from 'nock';
// Создаем перехватчик для API example.com
const scope = nock('https://api.example.com');
// Теперь можно добавлять правила для конкретных эндпоинтов
scope.get('/users').reply(200, []);
4.2. scope.get(path: string, requestBody?: RequestBodyMatcher, options?: RequestOptions)
Сигнатура: .get(path: string, requestBody?: string | RegExp | object | Function, options?: { ... }): Interceptor
Что делает: Перехватывает HTTP GET-запросы по указанному пути. Можно дополнительно указать тело запроса (для случаев, когда GET-запрос содержит тело, что редко, но возможно) или query-параметры.
nock('https://api.github.com')
.get('/repos/nodejs/node')
.reply(200, { name: 'node', stars: 100000 });
// Перехват с query-параметрами
nock('https://api.example.com')
.get('/search')
.query({ q: 'typescript', page: 1 })
.reply(200, { results: [] });
4.3. scope.post(path: string, requestBody?: RequestBodyMatcher, options?: RequestOptions)
Сигнатура: .post(path: string, requestBody?: string | RegExp | object | Function, options?: { ... }): Interceptor
Что делает: Перехватывает HTTP POST-запросы. Часто используется для мокирования создания ресурсов. Можно задать точное тело запроса, регулярное выражение или функцию для валидации.
nock('https://api.example.com')
.post('/users', { name: 'Alice', role: 'admin' })
.reply(201, { id: 123, name: 'Alice' });
// Использование функции для валидации тела
nock('https://api.example.com')
.post('/login', (body) => body.username === 'admin' && body.password.length > 5)
.reply(200, { token: 'abc123' });
4.4. interceptor.reply(statusCode: number, body?: Body, headers?: Headers)
Сигнатура: .reply(statusCode: number, body?: string | Buffer | object, headers?: Record<string, string>): Scope
Что делает: Определяет ответ, который будет возвращен на перехваченный запрос. Принимает HTTP-статус код, тело ответа и опциональные заголовки. Это основной способ задания ответа.
nock('https://api.example.com')
.get('/status')
.reply(200, { status: 'ok' }, { 'X-Custom-Header': 'value' });
// Ответ с задержкой (симуляция сети)
nock('https://api.example.com')
.get('/slow')
.delay(2000) // задержка 2 секунды
.reply(200, { data: 'slow response' });
4.5. interceptor.replyWithError(error: Error | string)
Сигнатура: .replyWithError(error: Error | string): Scope
Что делает: Симулирует ошибку на уровне сети (например, разрыв соединения, таймаут DNS). Полезно для тестирования обработки исключений, когда сервер недоступен.
nock('https://api.example.com')
.get('/unstable')
.replyWithError('Connection refused');
// Или с объектом Error
nock('https://api.example.com')
.get('/broken')
.replyWithError(new Error('Network timeout'));
4.6. scope.persist()
Сигнатура: .persist(flag?: boolean): Scope
Что делает: Делает перехватчик постоянным. По умолчанию nock перехватывает запрос только один раз, после чего "снимает" мок. Вызов .persist() позволяет использовать один и тот же мок многократно. Это удобно для мокирования часто вызываемых эндпоинтов.
// Мок будет работать для всех последующих GET запросов к /health
nock('https://api.example.com')
.get('/health')
.reply(200, { status: 'healthy' })
.persist();
// Теперь можно вызывать сколько угодно раз
await fetch('https://api.example.com/health'); // OK
await fetch('https://api.example.com/health'); // OK снова
4.7. scope.log(enableLogging: boolean)
Сигнатура: .log(enableLogging: boolean): Scope
Что делает: Включает или выключает логирование для данного scope. Когда логирование включено, nock выводит в консоль информацию о перехваченных запросах и ответах. Это очень полезно для отладки, когда вы не уверены, что ваш мок срабатывает.
nock('https://api.example.com')
.log(true) // Включаем логирование
.get('/debug')
.reply(200, { message: 'ok' });
// В консоли вы увидите:
// nock: intercept GET https://api.example.com/debug
// nock: matching GET https://api.example.com/debug
// nock: reply 200
4.8. nock.cleanAll()
Сигнатура: nock.cleanAll(): void
Что делает: Глобальная функция, которая удаляет все активные перехватчики и восстанавливает стандартное поведение HTTP. Обязательно вызывайте этот метод в хуке afterEach или afterAll вашего тестового фреймворка, чтобы избежать "загрязнения" тестов.
import nock from 'nock';
beforeEach(() => {
nock.cleanAll(); // Гарантируем чистоту перед каждым тестом
});
afterAll(() => {
nock.cleanAll(); // Финальная очистка
});
4.9. nock.restore()
Сигнатура: nock.restore(): void
Что делает: Полностью отключает перехват HTTP-запросов, восстанавливая оригинальные модули http и https. В отличие от cleanAll, который удаляет только правила, restore выключает сам механизм перехвата. Используйте это, если хотите временно отключить nock в середине теста.
nock('https://api.example.com')
.get('/data')
.reply(200, { mocked: true });
// Выключаем перехват
nock.restore();
// Этот запрос пойдет в реальный интернет
const response = await fetch('https://api.example.com/data');
// Включаем обратно
nock.activate(); // Включает перехват снова
4.10. scope.intercept(path: string, method: string, requestBody?: RequestBodyMatcher, options?: RequestOptions)
Сигнатура: .intercept(path: string, method: string, requestBody?: string | RegExp | object | Function, options?: { ... }): Interceptor
Что делает: Универсальный метод для перехвата запросов с любым HTTP-методом (GET, POST, PUT, DELETE, PATCH и т.д.). Полезен, когда метод определяется динамически.
nock('https://api.example.com')
.intercept('/items', 'DELETE')
.reply(204);
// Эквивалентно:
nock('https://api.example.com')
.delete('/items')
.reply(204);
4.11. interceptor.times(number: number)
Сигнатура: .times(number: number): Interceptor
Что делает: Ограничивает количество раз, которое данный перехватчик может быть использован. По умолчанию — 1 раз (если не используется persist). Позволяет точно контролировать, сколько запросов будет перехвачено.
nock('https://api.example.com')
.get('/retry')
.times(3) // Перехватит первые 3 запроса
.reply(500, { error: 'Retry later' });
// Четвертый запрос пойдет в реальный сервер (или вызовет ошибку, если не настроено)
4.12. interceptor.delay(delayInMs: number)
Сигнатура: .delay(delayInMs: number): Interceptor
Что делает: Добавляет задержку перед отправкой ответа. Полезно для тестирования таймаутов и асинхронного поведения