В чем разница между моком (mock) и стабом (stub) в модульном тестировании?
Я прочитал различные статьи о мокинге (mocking) и стаббинге (stubbing) в тестировании, включая статью Мартина Фаулера “Mocks Aren’t Stubs”, но все еще не понимаю разницы. Может кто-нибудь объяснить ключевые различия между моками (mocks) и стабами (stubs), их случаи использования и когда применять каждый из них в тестовых сценариях?
Моки и стабы: различия и применение в тестировании
Моки и стабы — это оба вида тестовых даблов, используемых в модульном тестировании для изоляции тестируемого кода, но они служат разным целям: моки проверяют взаимодействия и поведение, в то время как стабы предоставляют предопределенные ответы на вызовы методов. Ключевое различие заключается в их подходе к тестированию — моки фокусируются на проверке “как” код взаимодействует с зависимостями, в то время как стабы фокусируются на предоставлении “что” возвращают зависимости. Понимание этого различия помогает разработчикам выбирать правильный тестовый дабл для конкретных сценариев.
Содержание
- Понимание тестовых даблов
- Что такое стаб?
- Что такое мок?
- Ключевые различия между моками и стабами
- Когда использовать стабы
- Когда использовать моки
- Практические примеры
- Лучшие практики
Понимание тестовых даблов
Тестовые даблы — это объекты, которые заменяют реальные зависимости во время тестирования, позволяя разработчикам изолировать тестируемый код от внешних систем. Термин “тестовый дабл” был введен Джерардом Мезаросом в его книге xUnit Test Patterns и является аналогией из киноиндустрии, где дублеры заменяют актеров.
Существует несколько типов тестовых даблов, включая:
- Стабы: Предоставляют заготовленные ответы на вызовы методов
- Моки: Проверяют взаимодействия и поведение
- Фейки: Упрощенные рабочие реализации
- Дамми: Объекты, которые передаются, но никогда не используются
- Шпионы: Отслеживают вызовы методов без влияния на их поведение
Каждый тип служит конкретной цели в тестировании, при этом стабы и моки являются наиболее часто используемыми в современных практиках тестирования.
Что такое стаб?
Стаб — это упрощенная реализация зависимости, которая возвращает предопределенные ответы на вызовы методов. Согласно PFLB, стабы “используются в мок-тестировании для имитации поведения реальных объектов и обеспечения выполнения определенных путей кода во время тестирования.”
Характеристики стабов:
- Возвращают предопределенные значения для конкретных вызовов методов
- Не проверяют, как вызываются методы
- Фокусируются на тестировании на основе состояния
- Обычно являются пассивными объектами
- Предоставляют предсказуемые, последовательные ответы
Например, если вы тестируете сервис пользователей, который зависит от базы данных, вы можете создать стаб подключения к базе данных, который возвращает определенные данные пользователя при запросе, без фактического подключения к реальной базе данных.
// Пример стаба
const databaseStub = {
findUser: (id) => {
if (id === '123') return { id: '123', name: 'John Doe' };
if (id === '456') return { id: '456', name: 'Jane Smith' };
return null;
}
};
Стабы особенно полезны, когда вам нужно протестировать различные сценарии, изменяя ответы от зависимостей, например, для проверки того, как ваш код обрабатывает отсутствующие данные, ошибки или определенные состояния данных.
Что такое мок?
Объект мока — это более сложный объект, чем стаб — он не только предоставляет ответы, но и проверяет, что тестируемый код взаимодействует с ним правильно. Как объясняет Harness, моки изолируют “систему под тестированием от ненадежных или незавершенных зависимостей” и могут проверять ожидаемые взаимодействия.
Характеристики моков:
- Проверяют, что методы вызываются с определенными параметрами
- Отслеживают количество и порядок вызовов методов
- Могут генерировать исключения при неожиданных взаимодействиях
- Фокусируются на поведенческом тестировании
- Обычно являются активными объектами, которые утверждают свои ожидания
Например, использование мока для проверки того, что сервис электронной почты вызывается ровно один раз с правильными параметрами при регистрации пользователя:
// Пример мока
const emailServiceMock = {
sendWelcomeEmail: jest.fn(),
verify: (expectedCalls) => {
expect(emailServiceMock.sendWelcomeEmail).toHaveBeenCalledTimes(expectedCalls.count);
expect(emailServiceMock.sendWelcomeEmail).toHaveBeenCalledWith(...expectedCalls.args);
}
};
Моки становятся мощными инструментами, когда вам нужно проверить, что ваш код следует определенным шаблонам взаимодействия, например, ensuring that cleanup operations are performed, или что определенные методы вызываются в правильном порядке.
Ключевые различия между моками и стабами
Фундаментальное различие между моками и стабами заключается в их философии тестирования и том, что они проверяют:
| Аспект | Стабы | Моки |
|---|---|---|
| Цель | Предоставление предопределенных ответов | Проверка взаимодействий и поведения |
| Подход к тестированию | На основе состояния | На основе поведения |
| Проверка | Проверка возвращаемых значений и состояния | Проверка вызовов методов и порядка |
| Сложность | Простые, пассивные объекты | Сложные, активные объекты |
| Фокус | “Что” возвращает зависимость | “Как” код взаимодействует с зависимостью |
| Настройка | Настройка возвращаемых значений | Настройка ожиданий и проверок |
Как отмечено в руководстве по тестированию MoldStud, “Если вы хотите проверить взаимодействие без выполнения фактического кода, создайте стабы, которые возвращают предопределенные значения. С другой стороны, шпионы позволяют отслеживать, был ли метод вызван, сколько раз и с какими аргументами.”
Это различие имеет решающее значение: стабы помогают вам протестировать логику вашего кода, контролируя, что возвращают зависимости, в то время как моки помогают вам протестировать поведение вашего кода, проверяя, как он использует эти зависимости.
Взгляд Фаулера
Хотя оригинальная статья Мартина Фаулера “Моки — это не стабы” напрямую недоступна в наших результатах поиска, различие, которое он популяризировал, является фундаментальным для современных практик тестирования. Фаулер утверждал, что:
- Стабы связаны с предоставлением тестовых данных и контролем того, что возвращают зависимости
- Моки связаны с проверкой взаимодействий и обеспечением правильного использования зависимостей тестируемым кодом
Это различие помогает предотвратить то, что Фаулер называл “мокистским” тестированием (злоупотребление моками для всего) в противовес “классическому” тестированию (более сфокусированному на проверке состояния).
Когда использовать стабы
Стабы являются правильным выбором в следующих сценариях:
Тестирование различных сценариев
Когда вам нужно протестировать, как ваш код обрабатывает различные ответы от зависимостей:
- Отсутствующие данные
- Условия ошибок
- Разные состояния данных
- Крайние случаи
Например, тестирование сервиса регистрации пользователя со стабом, который возвращает разные результаты существования пользователя:
// Стаб для тестирования различных сценариев
const userExistsStub = {
checkUserExists: (email) => {
// Имитация различных сценариев
if (email === 'existing@example.com') return true;
if (email === 'error@example.com') throw new Error('Ошибка базы данных');
return false; // Новый пользователь
}
};
Тестирование логики на основе состояния
Когда правильность вашего кода зависит от состояния, которое он достигает, а не от того, как он его достигает:
- Вычисление итоговых значений из нескольких источников данных
- Обработка и преобразование данных
- Создание отчетов или выходных данных
Внешние зависимости
При работе с:
- Базами данных и хранилищами данных
- Веб-сервисами и API
- Файловыми системами
- Сетевыми ресурсами
Стабы позволяют тестировать эти сценарии без фактического подключения к внешним системам, делая тесты быстрее и надежнее.
Когда использовать моки
Моки особенно ценны в следующих ситуациях:
Проверка шаблонов взаимодействия
Когда вам нужно убедиться, что ваш код правильно использует зависимости:
- Метод вызывается с правильными параметрами
- Метод вызывается ожидаемое количество раз
- Методы вызываются в правильном порядке
- Выполняются операции очистки
Например, тестирование того, что сервис платежей правильно вызывает сервис авторизации перед обработкой:
// Мок для проверки шаблонов взаимодействия
const authServiceMock = {
authorize: jest.fn().mockReturnValue(true),
verify: () => {
expect(authServiceMock.authorize).toHaveBeenCalledBefore(paymentServiceMock.processPayment);
}
};
Тестирование сложных рабочих процессов
Для многошаговых процессов, где порядок взаимодействия имеет значение:
- Потоки регистрации пользователей
- Конвейеры обработки платежей
- Рабочие процессы преобразования данных
Поведенческие контракты
Когда вы хотите обеспечить поведенческие контракты между компонентами:
- Контракты API между сервисами
- Точки интеграции между модулями
- Реализации интерфейсов
Моки помогают обеспечить соблюдение этих контрактов, предотвращая регрессии при изменении кода.
Практические примеры
Рассмотрим несколько практических примеров, иллюстрирующих, когда использовать моки, а когда стабы.
Пример 1: Сервис регистрации пользователя
Сценарий: Сервис регистрации пользователя, который должен:
- Проверить, существует ли пользователь уже
- Хешировать пароль
- Сохранить пользователя в базу данных
- Отправить приветственное письмо
Использование стабов:
describe('Сервис регистрации пользователя со стабами', () => {
it('должен регистрировать нового пользователя', () => {
// Стабы зависимостей
const userRepoStub = {
findByEmail: () => null, // Пользователь не существует
save: (user) => user // Успешно сохраняет
};
const passwordHasherStub = {
hash: (password) => 'hashed_password'
};
const emailServiceStub = {
sendWelcomeEmail: () => {} // Ничего не делает
};
// Тестируем логику сервиса
const service = new RegistrationService(
userRepoStub, passwordHasherStub, emailServiceStub
);
const result = service.register('test@example.com', 'password123');
// Проверяем результат
expect(result.success).toBe(true);
expect(result.user.email).toBe('test@example.com');
});
});
Использование моков:
describe('Сервис регистрации пользователя с моками', () => {
it('должен вызывать зависимости в правильном порядке', () => {
// Моки зависимостей с ожиданиями
const userRepoMock = {
findByEmail: jest.fn(),
save: jest.fn()
};
const passwordHasherMock = {
hash: jest.fn().mockReturnValue('hashed_password')
};
const emailServiceMock = {
sendWelcomeEmail: jest.fn()
};
const service = new RegistrationService(
userRepoMock, passwordHasherMock, emailServiceMock
);
service.register('test@example.com', 'password123');
// Проверяем взаимодействия
expect(userRepoMock.findByEmail).toHaveBeenCalledWith('test@example.com');
expect(passwordHasherMock.hash).toHaveBeenCalledWith('password123');
expect(userRepoMock.save).toHaveBeenCalledWith(
expect.objectContaining({ email: 'test@example.com' })
);
expect(emailServiceMock.sendWelcomeEmail).toHaveBeenCalled();
// Проверяем порядок вызовов
expect(userRepoMock.findByEmail).toHaveBeenCalledBefore(passwordHasherMock.hash);
expect(passwordHasherMock.hash).toHaveBeenCalledBefore(userRepoMock.save);
});
});
Пример 2: Сервис обработки платежей
Сценарий: Сервис платежей, который обрабатывает заказы через несколько шагов.
Подход со стабами (фокус на состоянии):
describe('Обработка платежей со стабами', () => {
it('должен обрабатывать платеж успешно', () => {
// Стаб платежного шлюза для возврата успеха
const paymentGatewayStub = {
process: () => ({ success: true, transactionId: 'txn_123' })
};
const orderRepositoryStub = {
updateStatus: () => {} // Обновляет статус заказа
};
const service = new PaymentService(
paymentGatewayStub, orderRepositoryStub
);
const order = { id: 'ord_456', amount: 100 };
const result = service.processPayment(order);
// Проверяем конечное состояние
expect(result.success).toBe(true);
expect(result.transactionId).toBeDefined();
});
});
Подход с моками (фокус на поведении):
describe('Обработка платежей с моками', () => {
it('должен правильно обрабатывать ошибки платежа', () => {
// Мок платежного шлюза для генерации исключения
const paymentGatewayMock = {
process: jest.fn().mockImplementation(() => {
throw new Error('Недостаточно средств');
})
};
const orderRepositoryMock = {
updateStatus: jest.fn()
};
const service = new PaymentService(
paymentGatewayMock, orderRepositoryMock
);
const order = { id: 'ord_456', amount: 100 };
expect(() => service.processPayment(order)).toThrow('Недостаточно средств');
// Проверяем, что статус заказа был обновлен на "неудачный"
expect(orderRepositoryMock.updateStatus).toHaveBeenCalledWith(
order.id, 'payment_failed'
);
});
});
Лучшие практики
Выбор между моками и стабами
- Начинайте со стабов: Начинайте со стабов в большинстве сценариев, так как они проще и более поддерживаемы
- Используйте моки умеренно: Используйте моки только тогда, когда вам конкретно нужно проверить взаимодействия
- Учитывайте пирамиду тестирования: Используйте больше модульных тестов со стабами и меньше интеграционных тестов с моками
- Избегайте излишнего мокирования: Не мокируйте всё — сосредоточьтесь на наиболее критичных взаимодействиях
Советы по реализации
- Четкое именование: Используйте описательные имена для тестовых даблов (например,
userRepositoryStub,paymentServiceMock) - Настройка и очистка: Правильно инициализируйте и очищайте тестовые даблы
- Документация: Комментируйте, почему вы используете определенный тип тестового дабла
- Последовательность: Будьте последовательны в вашем тестовом наборе
Распространенные ошибки, которых следует избегать
- Слишком много мокирования: Это может привести к хрупким тестам, которые ломаются при изменениях реализации
- Путаница состоянием и поведением: Не используйте моки, когда вам нужно протестировать только состояние
- Избыточная спецификация: Избегайте тестирования деталей реализации, которые могут измениться
- Игнорирование скорости тестов: Моки иногда могут замедлять тесты из-за накладных расходов на проверку
Понимание фундаментальных различий между моками и стабами позволяет вам писать более эффективные тесты, которые фокусируются на правильных аспектах вашего кода. Стабы помогают вам контролировать, что возвращают зависимости, в то время как моки помогают вам проверять, как ваш код использует эти зависимости. Ключ — выбрать правильный инструмент для конкретного тестового сценария.
Источники
- PFLB - Что такое мок-тестирование?: Преимущества и как это работает
- Harness - Что такое мок-тестирование?
- GFU Wiki - Was ist Mocking?
- MoldStud - Техники мокирования и стаббинга в Node.js тестировании с Mocha и Chai
- dblp - Martin Fowler
- Lotus QA - Модульное тестирование против функционального тестирования: Комплексное сравнение
Заключение
Понимание различия между моками и стабами имеет решающее значение для написания эффективных модульных тестов. Вот ключевые выводы:
-
Стабы предоставляют предопределенные ответы и фокусируются на тестировании на основе состояния, что делает их идеальными для тестирования того, как ваш код обрабатывает различные сценарии и состояния данных.
-
Моки проверяют взаимодействия и фокусируются на поведенческом тестировании, что делает их идеальными для обеспечения правильного использования зависимостей вашим кодом с точки зрения вызовов методов, параметров и порядка.
-
Выбирайте на основе того, что вы тестируете: используйте стабы, когда вас интересует “что” происходит с состоянием, и моки, когда вас интересует “как” происходят взаимодействия.
-
Сбалансируйте свой подход: не злоупотребляйте моками — они могут сделать тесты хрупкими. Начинайте со стабов и вводите моки только тогда, когда вам конкретно нужно проверить взаимодействия.
-
Учитывайте контекст тестирования: для простых сценариев часто достаточно стабов. Для сложных рабочих процессов или поведенческих контрактов моки становятся более ценными.
Освоив обе техники и зная, когда применять каждую, вы создадите тесты, которые более поддерживаемы, сфокусированы и эффективны в обнаружении правильных видов ошибок в вашем коде.