Вступление
Когда заходит разговор об автоматизации тестирования, слово «мок» (mock) становится универсальным ответом на любой вопрос. Нужно заменить базу данных? «Замокаем». Внешнее API тормозит? «Повесим мок». Сторонний сервис ещё не написан? «Ну, сделаем мок».
Проблема в том, что в 90% случаев, когда разработчик или тестировщик говорит «мок», он имеет в виду что-то другое. И это не просто вопрос душной терминологии.
Подмена понятий в инструментах тестирования приводит к тому, что тесты становятся либо слишком хрупкими и падают от любого рефакторинга, либо бесполезными, потому что проверяют не логику, а наличие вызова функции. Вы вроде бы покрыли код тестами, но при малейшем изменении внутренней структуры системы всё «краснеет», хотя бизнес-логика осталась прежней. Или наоборот: тесты «зелёные», а на проде всё падает, потому что заглушка вела себя слишком идеально.
Чтобы тесты приносили пользу, а не только увеличивали время сборки в CI/CD, нужно четко понимать, какой «дублер» нужен в конкретной ситуации.
В этой статье мы разберем на практике, в чем реальная разница между Stub, Mock и Fake, на каких уровнях тестирования их применять и как перестать писать тесты, которые проверяют сами себя.
Классическая терминология
Прежде чем переходить к практическим примерам, полезно зафиксировать классическую терминологию.
В литературе по тестированию Stub, Mock и Fake — это разные виды test doubles, то есть тестовых дублеров, которыми мы заменяем реальные зависимости системы.
Stub — это дублер, который возвращает заранее подготовленные данные. Его задача — дать системе контролируемый ответ, чтобы тест мог пройти по нужному сценарию.
Mock — это дублер, который не только подменяет зависимость, но и позволяет проверять взаимодействие с ней: был ли вызов, сколько раз он произошел, с какими параметрами.
Fake — это упрощенная, но рабочая реализация зависимости. У нее обычно есть состояние и внутренняя логика, но она не подходит для продакшена. Например, in-memory база данных, локальное файловое хранилище или упрощенная очередь сообщений.
На практике терминология часто используется менее строго. Особенно это заметно в интеграционных тестах, где вместо внутрипроцессного объекта может использоваться отдельный HTTP-сервис, который одновременно и возвращает подготовленные ответы, и сохраняет историю вызовов.
Поэтому ниже я сначала опираюсь на классические определения, а затем показываю более прикладные примеры, которые ближе к реальной инженерной практике.
Стабы (Stubs): когда нужен контролируемый ответ
Стаб (stub) — это дублер, который возвращает заранее подготовленные ответы во время теста. Его задача — дать системе предсказуемый вход из внешней зависимости, чтобы мы могли проверить собственную логику в контролируемом сценарии.
Обычно стаб не используют для проверки количества вызовов или параметров обращения. Его основная роль — быть источником заранее известных данных.
Реальный пример: внешний stub-сервер на FastAPI
Представьте: вы пишете автотест для сервиса заказов. Чтобы оформить заказ, ваше приложение обращается во внешний сервис «Склад» (Warehouse) и проверяет, есть ли товар в наличии.
Настоящий сервис склада в тестовой среде может тормозить, быть временно недоступным или содержать неподходящие данные. Чтобы тест оставался стабильным и воспроизводимым, мы поднимаем свой stub-сервер на FastAPI, который всегда возвращает заранее согласованный ответ.
1. Код нашего Стаб-сервера (Warehouse Stub):
from fastapi import FastAPI app = FastAPI() @app.get("/inventory/{item_id}") async def get_inventory(item_id: int): # Это стаб. Он не идет в реальную базу. # Он просто возвращает захардкоженные данные. return {"item_id": item_id, "status": "IN_STOCK", "quantity": 100}
2. Код автотеста (на Pytest):
import requests def test_order_creation_when_item_in_stock(): # В конфигурации order-service уже указан адрес stub-сервера склада: # WAREHOUSE_URL=http://localhost:8081 # Act: вызываем именно нашу систему, а не сам stub response = requests.post( "http://localhost:8000/orders", json={"item_id": 1, "user_id": 42} ) data = response.json() # Assert: проверяем поведение order-service, # который внутри сходил во внешний Warehouse Stub assert response.status_code == 201 assert data["status"] == "CREATED" assert data["reserved_quantity"] == 1
В этом сценарии FastAPI-приложение выступает как Stub. Оно имитирует контракт реального сервиса склада, но сам тест проверяет не заглушку, а поведение order-service, который использует этот контролируемый ответ.
Это и есть ключевая идея стаба: он нужен не для того, чтобы тестировать самого себя, а для того, чтобы создать предсказуемые условия для проверки вашей системы.
Когда использовать стабы в автотестах:
Изоляция от внешних API. Чтобы не зависеть от сети, нестабильных тестовых сред и сторонних сервисов.
Контроль негативных сценариев. Когда нужно воспроизвести 404, 500, timeout или любой другой ответ, который трудно стабильно получить от реальной системы.
Параллельная разработка. Когда контракт уже согласован, а реальный сервис еще не готов.
Ускорение тестов. Стаб обычно быстрее и предсказуемее тяжелых внешних систем.
Главное отличие: Стаб vs Мок
Разница между stub и mock — не только в синтаксисе, но и в том, на чем сфокусирован тест.
Если вам важно, какой ответ получила система от внешней зависимости и как она его обработала, чаще всего нужен stub.
Если вам важно проверить само взаимодействие: был ли вызов, сколько раз он произошел, с какими параметрами, — это уже ближе к mock-подходу.
Иначе говоря: stub помогает контролировать входные данные для теста, а mock помогает проверять поведение системы по отношению к зависимости.
Моки (Mocks): когда нужно проверить взаимодействие
В классическом понимании mock — это дублер, который используется для проверки взаимодействия с зависимостью: был ли вызов, сколько раз он произошел и с какими параметрами.
На практике, особенно в интеграционных тестах, роль такого дублера может выполнять не только объект в памяти, но и отдельный HTTP-сервис, который сохраняет историю вызовов. Формально это уже ближе к mock server или test double, но по смыслу задача остается той же: проверить корректность взаимодействия.
Если stub в первую очередь поставляет контролируемые данные, то mock сфокусирован на проверке взаимодействия с зависимостью. Его задача — не только подменить зависимость, но и дать тесту возможность проверить, как именно система к ней обращалась: сколько раз, с какими параметрами и в каком контексте.
Используя мок, вы проверяете взаимодействие с внешней системой и корректность вызовов. Вам важно убедиться, что система не просто получила ответ, а корректно обратилась к внешней зависимости.
Реальный пример: внешний mock-сервер на FastAPI с сохранением истории вызовов
Представьте: вы тестируете отправку SMS через внешний шлюз. Вам нужно не только имитировать ответ шлюза, но и убедиться, что ваша система не отправила 10 SMS вместо одного (защита от дублей).
Для этого мы поднимаем мок-сервер, который умеет две вещи:
Отдавать динамический JSON из файла.
Сохранять историю всех входящих запросов в список (логировать), чтобы мы могли их проверить.
1. Код Мок-сервера (SMS Gateway Mock):
from fastapi import FastAPI, Request import json app = FastAPI() # Наш "журнал посещений" — сохраняем сюда все вызовы requests_history = [] @app.post("/send_sms") async def send_sms_mock(request: Request): payload = await request.json() # Мок фиксирует факт вызова и сохраняет данные запроса requests_history.append(payload) # Динамически отдаем ответ из файла with open("sms_success_response.json") as f: return json.load(f) @app.get("/_internal/history") async def get_history(): # Эндпоинт для нашего автотеста, чтобы проверить "шпиона" return requests_history
2. Код автотеста (на Pytest):
import requests def test_sms_notification_integrity(): # 1. Act: Запускаем бизнес-процесс (например, через API вашего приложения) # Ваше приложение внутри себя дергает наш Мок-сервер (localhost:8082) trigger_notification_api(user_id=777) # 2. Assert: Идем к моку и спрашиваем его "журнал" history_response = requests.get("http://localhost:8082/_internal/history") history = history_response.json() # Проверяем поведение системы (Behavior Verification) assert len(history) == 1, "SMS было отправлено больше одного раза!" assert history[0]["phone"] == "+79991234567" assert "Ваш код: 1234" in history[0]["text"]
В этом сценарии FastAPI-приложение выступает как внешний test double с возможностью проверки вызовов. Мы не просто подменили SMS-шлюз, а еще и проверили, сколько раз и с какими данными наше приложение к нему обратилось.
С практической точки зрения это mock-подход: тест проверяет не только результат, но и корректность взаимодействия с внешней системой.
Когда использовать моки в автотестах:
Проверка отсутствия дублей: Чтобы не списать деньги дважды или не отправить 100 пушей пользователю.
Сложная логика параметров: Когда важно проверить, что в Headers ушли правильные токены, а в Body — валидный JSON.
Гарантия вызова: Если внешняя система ничего не возвращает (fire and forget), единственный способ проверить работу — посмотреть логи в моке.
Динамика по ID: Мок может искать нужный файл ответа на основе
id, который прислало приложение.
Главное отличие: Мок vs Стаб
Stub и mock решают разные задачи.
Stub нужен, когда вы хотите управлять ответом внешней зависимости и проверить, как ваша система обработает этот ответ.
Mock нужен, когда вам важно проверить само взаимодействие с зависимостью: был ли вызов, не было ли дублей, корректно ли сформировались параметры запроса.
Если упростить: stub отвечает на вопрос «что вернула внешняя система?», а mock — на вопрос «как именно наша система с ней взаимодействовала?».
Фейки (Fakes): почти как настоящие, но проще
Фейк (fake) — это упрощенная, но рабочая реализация зависимости. У него есть собственное состояние и логика, поэтому он ведет себя ближе к реальной системе, чем stub или mock. При этом fake не предназначен для продакшена: он создается именно как тестовая замена.
В отличие от стабов и моков, у фейка есть реальная бизнес-логика внутри. Если вы положите данные в фейковую базу данных, вы сможете их оттуда прочитать. Если вы отправите сообщение в фейковую очередь, оно там будет лежать, пока его не заберут. Но такой дублер обычно не решает продакшен-задачи вроде надежного хранения данных, масштабирования, отказоустойчивости или безопасности — он нужен именно для тестового сценария.
Реальный пример: SQLite в памяти вместо тяжелого Postgres
Представьте: вы пишете интеграционные тесты для сервиса, который активно работает с базой данных (хранит заказы, обновляет остатки). Поднимать каждый раз реальный экземпляр Postgres в Docker — это долго (нужно ждать инициализации, накатывать миграции, чистить данные).
В качестве иллюстрации идеи fake можно использовать SQLite в режиме :memory:. Это рабочая база данных с собственным состоянием и SQL-операциями, которая живет только во время теста и запускается намного быстрее полноценного Postgres.
Важно помнить, что SQLite не повторяет Postgres полностью, поэтому такой подход хорошо подходит для демонстрации концепции и для части тестов, но не заменяет проверки на реальной СУБД там, где важны особенности конкретного движка.
1. Код инициализации БД (интерфейс):
import sqlite3 class OrderRepository: def __init__(self, db_connection): self.conn = db_connection def save_order(self, order_id, amount): cursor = self.conn.cursor() cursor.execute("INSERT INTO orders VALUES (?, ?)", (order_id, amount)) self.conn.commit() def get_order_amount(self, order_id): cursor = self.conn.cursor() cursor.execute("SELECT amount FROM orders WHERE id = ?", (order_id,)) return cursor.fetchone()[0]
2. Код автотеста с использованием Фейка:
import pytest def test_order_storage_logic(): # Создаем ФЕЙК — настоящую БД, но живущую только в оперативной памяти fake_db = sqlite3.connect(":memory:") fake_db.execute("CREATE TABLE orders (id INT, amount INT)") repo = OrderRepository(fake_db) # Act: Мы реально сохраняем данные repo.save_order(order_id=1, amount=500) # Assert: И мы реально их читаем обратно, проверяя логику БД saved_amount = repo.get_order_amount(order_id=1) assert saved_amount == 500 # Нам не нужно было настраивать стаб на возврат 500. # Система сработала "по-настоящему".
В этом сценарии SQLite — это Fake. Мы не программировали его отвечать «500» (как стаб) и не проверяли, сколько раз вызвался INSERT (как мок). Мы просто использовали упрощенную замену реальной БД.
Когда использовать фейки в автотестах:
In-memory реализации. Например, SQLite, H2 или другие упрощенные хранилища, когда вам нужна рабочая зависимость, но без затрат на полноценную инфраструктуру.
Фейковые хранилища (S3): Вместо отправки файлов в реальное облако AWS S3, используйте локальную папку или библиотеку
moto, которая имитирует поведение S3 в памяти.Фейковые очереди сообщений: Вместо RabbitMQ или Kafka можно использовать простую Python-очередь (queue.Queue), которая хранит сообщения в памяти во время теста.
In-memory кэш: Вместо реального Redis используйте обычный Python-словарь (
dict), обернутый в интерфейс кэша.
Главное отличие: Фейк vs Остальные
Stub и mock обычно описывают заранее заданное поведение зависимости и не стремятся воспроизвести ее внутреннюю логику.
Fake — это упрощенная, но работающая реализация. У него есть состояние, а поведение определяется не только настройкой теста, но и собственной логикой.
На пальцах: Если вы вручную прописали ответ — это стаб. Если вы проверяете вызов — это мок. Если вы используете «легкую версию» реальной системы, которая реально работает — это фейк.
Итоговая шпаргалка: что и когда выбирать?

Чтобы не путаться в терминах, просто задайте себе вопрос: «Что я хочу проверить в этом тесте?». Ответ на него и определит нужный тип двойника.
Тип двойника | Краткая суть | Что проверяем в тесте (Assert) | Идеальный кейс |
Stub (Стаб) | Возвращает заранее подготовленные ответы. | Как система обработала контролируемый ответ зависимости. | Изоляция от внешнего API, проверка обработки 404/500 ошибок. |
Mock (Мок) | Дублер для проверки взаимодействия с зависимостью. | Был ли вызов, сколько раз он произошел, с какими параметрами. | Проверка отправки уведомлений, защита от лишних платных запросов. |
Fake (Фейк) | Упрощенная, но рабочая реализация зависимости. | Поведение рабочей зависимости с собственным состоянием. | Упрощенная рабочая замена зависимости, например SQLite in-memory или in-memory реализация кэша. |
Заключение
Разделение на stub, mock и fake — это не спор о терминах ради терминов, а способ осознанно проектировать тесты. Когда вы понимаете, что именно хотите проверить — обработку ответа, корректность взаимодействия или работу зависимости с состоянием, — становится проще выбрать подходящий тип дублера.
Это делает тесты не только быстрее, но и надежнее: они меньше зависят от случайностей среды и лучше отражают реальную цель проверки.
