Изоляция тестов

Изоляция тестов в Jest — это один из ключевых аспектов качественного тестирования, который позволяет удостовериться, что каждый тест выполняется в чистой, независимой среде. Это минимизирует вероятность ошибок, связанных с состоянием, которое может изменяться между тестами. В этой главе рассмотрим, как изолировать тесты, какие средства предоставляет Jest для этого и как правильно организовать тестовую среду.

Зачем нужна изоляция тестов?

Тесты должны быть независимыми. Изоляция необходима для того, чтобы изменения в одном тесте не влияли на другие. Когда тесты не изолированы, результат одного теста может зависеть от выполнения предыдущего. Это приводит к трудностям при отладке и снижает предсказуемость тестов.

Изоляция помогает:

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

Использование beforeEach и afterEach для настройки и очистки

Jest предоставляет хуки beforeEach и afterEach, которые позволяют настроить состояние перед каждым тестом и очистить его после. Эти функции важны для изоляции, так как они помогают гарантировать, что каждый тест начинается с чистого состояния.

Пример использования beforeEach:

let counter;

beforeEach(() => {
  counter = 0;
});

test('увеличивает счетчик', () => {
  counter += 1;
  expect(counter).toBe(1);
});

test('сбросит счетчик перед каждым тестом', () => {
  expect(counter).toBe(0);
});

В этом примере beforeEach гарантирует, что переменная counter будет сброшена перед каждым тестом, что исключает влияние одного теста на другой.

Пример использования afterEach:

let db;

beforeEach(() => {
  db = connectToDatabase();
});

afterEach(() => {
  db.close();
});

test('должен сохранять данные в базу', () => {
  db.save({ id: 1, name: 'test' });
  expect(db.find(1)).toEqual({ id: 1, name: 'test' });
});

Здесь afterEach используется для закрытия соединения с базой данных после каждого теста. Это важно для предотвращения утечек памяти и других побочных эффектов.

Использование beforeAll и afterAll

Иногда требуется выполнить настройку только один раз для всех тестов в наборе, например, при подключении к базе данных или серверу. Для этого можно использовать хуки beforeAll и afterAll, которые выполняются один раз до начала всех тестов и после завершения всех тестов в файле.

let server;

beforeAll(() => {
  server = startServer();
});

afterAll(() => {
  server.close();
});

test('тестирование с сервером', () => {
  const response = fetchDataFromServer(server);
  expect(response).toBe('success');
});

В этом случае сервер будет запущен только один раз перед всеми тестами, а не перед каждым из них. Это повышает производительность, когда настройка ресурсоемка, но при этом сохраняет изоляцию.

Мокирование зависимостей

Для полной изоляции тестов важно мокуировать внешние зависимости. Jest предоставляет встроенную поддержку моков с помощью функций jest.mock(), jest.spyOn() и других. Это позволяет заменить реальные зависимости на фиктивные, которые можно контролировать и отслеживать.

Мокирование модулей:

// realModule.js
export const fetchData = () => {
  return fetch('https://api.example.com/data')
    .then(response => response.json());
};

// mockModule.js
jest.mock('./realModule', () => ({
  fetchData: jest.fn(() => Promise.resolve({ data: 'mocked data' }))
}));

import { fetchData } from './realModule';

test('тест с мокированными данными', async () => {
  const data = await fetchData();
  expect(data).toEqual({ data: 'mocked data' });
});

В этом примере функция fetchData замещена на мок-версию, которая всегда возвращает заранее определённые данные. Это позволяет изолировать тесты от реальных внешних вызовов.

Изоляция состояния с использованием jest.resetAllMocks и jest.clearAllMocks

Для того чтобы убедиться, что моки не сохраняют своё состояние между тестами, можно использовать функции jest.resetAllMocks() и jest.clearAllMocks(). Они сбрасывают состояние всех моков, шпионов и таймеров.

  • jest.resetAllMocks() — сбрасывает все моки и шпионы.
  • jest.clearAllMocks() — очищает вызовы и состояния моков, не сбрасывая их реализацию.
afterEach(() => {
  jest.resetAllMocks();
});

test('проверка вызова функции', () => {
  const mockFn = jest.fn();
  mockFn();
  expect(mockFn).toHaveBeenCalled();
});

Это гарантирует, что каждый тест будет работать с чистыми мокающими функциями, без влияния предыдущих тестов.

Изоляция с использованием jest.resetModules()

Когда тесты зависят от модулей, которые кэшируются в процессе выполнения тестов, можно использовать jest.resetModules() для сброса модуля, чтобы он загружался заново. Это полезно, если тесты изменяют состояние или поведение импортированных модулей.

let myModule;

beforeEach(() => {
  jest.resetModules();
  myModule = require('./myModule');
});

test('проверка модуля', () => {
  expect(myModule.someFunction()).toBe('default value');
});

Здесь jest.resetModules() сбрасывает кэш всех импортированных модулей, чтобы каждый тест начинался с чистого состояния.

Использование изоляции в асинхронных тестах

Асинхронные тесты также требуют изоляции. В Jest для этого используется поддержка async/await и методов, таких как done или resolves. Эти механизмы позволяют правильно изолировать асинхронные операции и гарантировать, что тесты не будут зависеть от других.

test('асинхронная операция', async () => {
  const data = await fetchDataFromServer();
  expect(data).toEqual({ success: true });
});

При этом каждый тест начинается с чистого состояния благодаря хукам beforeEach или beforeAll, что гарантирует корректную работу всех асинхронных операций.

Заключение

Изоляция тестов в Jest — это важный аспект написания стабильных и предсказуемых тестов. С помощью хуков для подготовки и очистки, моков, а также инструментов для сброса состояния можно гарантировать, что каждый тест будет независимым. Это позволяет повысить качество тестирования, упростить отладку и сделать код более поддерживаемым в долгосрочной перспективе.