Тест-дублёры и хрупкие тесты

Как изолировать тест от медленных и непредсказуемых зависимостей и не сделать его при этом хрупким.

Тест-дублёры (mock, stub, fake) — это подставные объекты, которые заменяют настоящие зависимости (базу, сеть, время), чтобы тест был быстрым, изолированным и предсказуемым.

Зачем подменять зависимости

Юнит-тест должен проверять одну единицу в изоляции. Но функция может звонить в реальную базу, дёргать платёжный API или зависеть от текущего времени. Тогда тест становится медленным, требует сети и даёт разный результат в разные дни. Решение — подменить зависимость дублёром.

ДублёрИдея
StubВозвращает заранее заданный ответ («заглушка»)
MockЗаглушка + проверяет, что его вызвали как ожидалось
FakeУпрощённая рабочая реализация (например, словарь вместо БД)

Пример: stub вместо реальной зависимости

Функция считает приветствие в зависимости от времени суток. Чтобы тест не зависел от реальных часов, передаём «час» как параметр-стаб — фиксированное значение.

def greeting(hour):
    """Приветствие по часу суток (0-23). Час передаётся снаружи."""
    if hour < 6:
        return "Доброй ночи"
    if hour < 12:
        return "Доброе утро"
    if hour < 18:
        return "Добрый день"
    return "Добрый вечер"


# Вместо реального времени подставляем фиксированные часы (stub)
assert greeting(3) == "Доброй ночи"
assert greeting(9) == "Доброе утро"
assert greeting(15) == "Добрый день"
assert greeting(21) == "Добрый вечер"
print("Тест детерминирован: время подменено стабом, результат стабилен")

Вывод:

Тест детерминирован: время подменено стабом, результат стабилен

Обратите внимание: мы заранее спроектировали функцию так, чтобы час передавался, а не брался изнутри. Это называют «внедрением зависимости» — и оно делает код тестируемым без сложных моков.

Fake вместо настоящей базы

Простой словарь может играть роль «базы данных» в тесте — это fake. Он работает по-настоящему, но в памяти и мгновенно.

class FakeUserDB:
    """Поддельная БД в памяти вместо настоящей."""
    def __init__(self):
        self._data = {}
    def save(self, user_id, name):
        self._data[user_id] = name
    def get(self, user_id):
        return self._data.get(user_id)


def register(db, user_id, name):
    db.save(user_id, name)
    return db.get(user_id)


db = FakeUserDB()                       # fake вместо реальной БД
assert register(db, 1, "Анна") == "Анна"
print("Fake-БД сработала: тест быстрый и не трогает реальную базу")

Вывод:

Fake-БД сработала: тест быстрый и не трогает реальную базу

Опасность: хрупкие тесты

Перебор с моками ведёт к хрупким тестам — тем, что проверяют не результат, а внутреннюю реализацию: «функция вызвала метод X ровно дважды в таком порядке». Стоит чуть изменить устройство кода (не меняя поведения) — и такой тест ломается без реальной причины. Правило: проверяйте наблюдаемое поведение (результат), а не внутренние шаги. Меньше моков — крепче тесты.

Итог

  • Тест-дублёры (stub/mock/fake) изолируют тест от базы, сети и времени.
  • Внедрение зависимости делает код тестируемым без сложных моков.
  • Тесты на детали реализации хрупки — проверяйте результат, а не внутренние вызовы.
Проверьте себя
1. Зачем в тестах используют тест-дублёры (mock/stub/fake)?
AЧтобы усложнить тесты
BЧтобы изолировать единицу от медленных и непредсказуемых зависимостей (БД, сеть, время)
CЧтобы тесты работали медленнее
DЧтобы не писать assert
2. Чем fake отличается от stub?
AНичем
BFake — упрощённая, но рабочая реализация (словарь вместо БД); stub просто отдаёт заданный ответ
CFake всегда медленнее настоящей БД
DStub нельзя использовать в Python
3. Почему тесты на детали реализации хрупкие?
AОни слишком быстрые
BОни ломаются при изменении внутреннего устройства, даже когда поведение не изменилось
CОни не используют assert
DОни проверяют слишком много результатов
Поддержать проект