Deep Engineering
Средний·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·40 МИН

Тестирование: unittest, pytest и mock — что на самом деле проверяется

Вопросы про тесты проверяют не знание синтаксиса, а понимание того, что тест проверяет на самом деле. Прогон отвечает: упавшая проверка и исключение в тесте — разные исходы; patch('time.time') не подменяет время в модуле, который сделал from time import time; Mock без спецификации принимает вызов с неверным числом аргументов и метод с опечаткой в имени; опечатка в имени проверки m.called_once() на 3.11 проходит молча, а с 3.12 — ошибка; и unittest запускает тесты по алфавиту, а не в порядке объявления.

Полное техническое изложение

TL;DR

Тест — это функция, которая вызывает код и проверяет результат; основных исходов у него три: прошёл, упал на проверке или упал с исключением. В стандартной библиотеке — unittest с классами и методами assertEqual, на практике чаще — pytest с обычным assert. Внешние зависимости — время, сеть, базу — подменяют объектами Mock из unittest.mock.

Отсюда главное следствие: тест проверяет ровно то, что вы подменили, и легко подменить не то. patch('time.time') не меняет время в модуле, который сделал from time import time, — у того своё имя. Mock без спецификации принимает вызов с неверным числом аргументов и метод с опечаткой: Mock().chrage(100, 'RUB') прошёл молча, а create_autospec ответил AttributeError. Но «со спецификацией» — не одна гарантия, а несколько: Mock(spec=PaymentClient) отвергает несуществующее имя, а вызов charge(100) без валюты принимает; сигнатуру проверяет только create_autospec (или patch(..., autospec=True)). Главное утверждение теста — о результате кода; проверка вызова mock — дополнительная.

Дальше — то, что отличает понимание от пересказа. Опечатка в имени проверки может отключить проверку: m.called_once() на 3.11 проходит молча, с 3.12 — AttributeError, а m.was_called_once() молчит на всех версиях. unittest запускает тесты по алфавиту, а не в порядке объявления; setUp вызывается перед каждым тестом, setUpClass — один раз.

Порог входа
Перед уроком достаточно понимать
  • что такое функция, класс и исключение; классы и методы — в уроке о классах;
  • что делают import и from … import — в уроке «Модули и импорт»;
  • что такое декоратор (@имя над функцией) и контекстный менеджер (with …:) — на уровне использования; подробно — в уроках о декораторах и о контекстных менеджерах, они идут по линии позже;
  • как поставить пакет: pytest — сторонний, его ставят командой python -m pip install pytest, лучше в виртуальное окружение; об окружениях — в уроке «Окружения и зависимости», он идёт по линии позже. Для механизмов 1–5 pytest не нужен: там только стандартная библиотека.
Заранее знать не нужно
  • FAIL против ERROR, setUp и setUpClass, порядок тестов;
  • Mock, return_value, side_effect,patch и где патчить;
  • create_autospec, опечатки в проверках, parametrize и фикстуры pytest.

На чём проверено: прогоны bench/testing/basics.py и bench/testing/practice.py — CPython 3.11.15, 3.12.3, 3.13.7 и 3.14.7, linux/amd64; прогоны с pytest — CPython 3.13.7 и pytest 8.4.2. Блоки вывода в тексте — запись на 3.13.7, расхождения версий — в «Истории версий». Строки документации сверены 14.10.2026. На эту дату вышел Python 3.15 (9 октября 2026); урок на нём не запускался.

База: что такое тест и чем unittest отличается от pytest

Фрагмент: add — проверяемая функция, она определена в другом месте. Полный запускаемый пример — ниже, в разделе «Полный пример».

PYTHON
# unittest — стандартная библиотека
import unittest
 
class AddTest(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 2), 4)
 
# pytest — сторонний пакет, обычные функции и assert
def test_add():
    assert add(2, 2) == 4

Ответ уровня Junior на «как вы тестируете»: пишу тесты на pytest или unittest: вызываю функцию и проверяю результат; внешние зависимости подменяю моками; тесты должны быть независимыми и быстрыми. Это верно, и за этим ответом идут уточнения: чем отличаются упавший и сломанный тест, где подменять функцию, что будет, если ошибиться в имени проверки, и в каком порядке выполняются тесты. Механизмы ниже отвечают на них прогоном.

Механизм 1: прошёл, FAIL и ERROR

контракт языкаВывод — прогон bench/testing/basics.py, раздел 1, одинаковый на CPython 3.11–3.14.

Вывод bench/testing/basics.py, раздел 1 — четыре теста unittest к функции add:

1) unittest: FAIL против ERROR
   test_bare_assert -> FAIL: AssertionError
   test_errors      -> ERROR: TypeError: unsupported operand type(s) for +: 'int' and 'str'
   test_fails       -> FAIL: AssertionError: 4 != 5
   test_passes      -> ok
   итого: тестов 4, failures 2, errors 1

FAIL — проверка сработала и не совпала: код вернул 4, а ждали 5. Для unittest FAIL — это любое AssertionError, в том числе от голого assert, как в test_bare_assert. Это сигнал «код делает не то». ERROR — любое другое исключение; здесь TypeError вылетел ещё до проверки. Это сигнал «тест или код сломан». unittest считает FAIL и ERROR отдельно — failures и errors, — и при разборе падений стоит смотреть сначала на ошибки: проверка в таком тесте даже не выполнилась.

В pytest слова другие: исключение в теле теста — тоже FAILED, а ERROR бывает, когда сломалась фикстура, — в выводе pytest в разделе «Глубже» это сбой подготовки.

Механизм 2: Mock — объект, который соглашается на всё

контракт языкаВывод — прогон bench/testing/basics.py, раздел 2, одинаковый на CPython 3.11–3.14.

Вывод bench/testing/basics.py, раздел 2:

2) Mock: атрибуты, вызовы, return_value, side_effect
   api.get_user(1)                -> Mock
   api.get_user.call_args         -> call(1)
   api.get_user.call_count        -> 1
   assert_called_once_with(1)     -> прошло
   return_value = {...}; вызов    -> {'id': 1}
   side_effect=[10, 20, исключение] -> [10, 20, 'TimeoutError: нет ответа']

Mock создаёт атрибут на любое имя и принимает любой вызов, а результатом вызова возвращает return_value — по умолчанию это тоже Mock, один и тот же для всех вызовов. Каждый вызов он записывает — call_args, call_count — и умеет его проверить: assert_called_once_with. Поведение задают двумя полями: return_value — что вернуть, side_effect — что делать на каждый вызов: список значений по очереди или исключение. Так тестируют код, который ходит в сеть: side_effect=[…, TimeoutError(…)] проверяет, что код переживает таймаут, без всякой сети.

Механизм 3: patch — подменять там, где имя ищут

контракт языкаВывод — прогон bench/testing/basics.py, раздел 4, одинаковый на CPython 3.11–3.14. Почему from копирует ссылку — урок «Модули и импорт».

Два модуля возвращают текущее время. Первый пишет import time и вызывает time.time(), второй — from time import time и вызывает time(). Вывод bench/testing/basics.py, раздел 4:

4) patch: подменять там, где имя ищут
   patch('time.time'):     app_import.now() -> 42.0 | app_from.now() == 42.0 -> False
   patch('app_from.time'): app_from.now()   -> 42.0

patch('модуль.имя') заменяет одно имя в одном модуле на время теста. app_import при каждом вызове ищет функцию в модуле time — подмена видна. app_from при импорте скопировал ссылку на настоящую функцию в своё имя time и в модуль time больше не заглядывает. Поэтому правило: патчить имя в том модуле, который его использует, — app_from.time, а не time.time. Это прямое следствие того, что from … import копирует ссылку.

Механизм 4: спецификация — spec ограничивает имена, autospec проверяет вызов

контракт языкаЧто делают spec и spec_set — строка документации Mock; проверка сигнатуры — строка документации create_autospec; вывод — прогон bench/testing/basics.py, разделы 5 и 7, одинаковый на CPython 3.11–3.14.

У PaymentClient один метод — charge(amount, currency). Вывод bench/testing/basics.py, раздел 5:

5) спецификация: Mock против create_autospec
   Mock().charge(100)                -> принято
   autospec.charge(100)              -> TypeError: missing a required argument: 'currency'
   autospec.charge(100, 'RUB')       -> принято
   Mock().chrage(100, 'RUB')         -> принято
   autospec.chrage(100, 'RUB')       -> AttributeError: Mock object has no attribute 'chrage'

Голый Mock принял и вызов без валюты, и метод с опечаткой: тест пройдёт, а настоящий код упадёт. create_autospec строит mock по настоящему классу: у подменяемых методов have their arguments checked to check that they are called with the correct signature (проверяются аргументы — вызваны ли они с правильной сигнатурой), а несуществующие атрибуты — ошибка. Та же защита — patch(..., autospec=True). Mock без спецификации — главный источник тестов, которые проходят, но ничего не проверяют.

Но «mock со спецификацией» — это три разных параметра с разными гарантиями, и путать их легко. spec — список допустимых имён: Accessing any attribute not in this list will raise an AttributeError (обращение к любому атрибуту не из этого списка вызывает AttributeError). spec_set — A stricter variant of spec (более строгий вариант spec): запрещает ещё и присваивать новые имена. О сигнатуре вызова в описании обоих не сказано ничего — её проверяет только create_autospec. Прогон, раздел 7, каждая строка — на свежем mock:

7) Mock, spec, spec_set, autospec: какие проверки включает каждый
   (у PaymentClient один метод charge(amount, currency); mock на каждую строку свежий)
   существование атрибута: m.chrage
      Mock()                                        -> принято
      Mock(spec=PaymentClient)                      -> AttributeError: Mock object has no attribute 'chrage'
      Mock(spec_set=PaymentClient)                  -> AttributeError: Mock object has no attribute 'chrage'
      create_autospec(PaymentClient, instance=True) -> AttributeError: Mock object has no attribute 'chrage'
   присваивание нового атрибута: m.extra = 1
      Mock()                                        -> принято
      Mock(spec=PaymentClient)                      -> принято
      Mock(spec_set=PaymentClient)                  -> AttributeError: Mock object has no attribute 'extra'
      create_autospec(PaymentClient, instance=True) -> принято
   сигнатура вызова: m.charge(100) без currency
      Mock()                                        -> принято
      Mock(spec=PaymentClient)                      -> принято
      Mock(spec_set=PaymentClient)                  -> принято
      create_autospec(PaymentClient, instance=True) -> TypeError: missing a required argument: 'currency'
   опечатка в проверке: m.was_called_once()
      Mock()                                        -> принято
      Mock(spec=PaymentClient)                      -> AttributeError: Mock object has no attribute 'was_called_once'
      Mock(spec_set=PaymentClient)                  -> AttributeError: Mock object has no attribute 'was_called_once'
      create_autospec(PaymentClient, instance=True) -> AttributeError: Mock object has no attribute 'was_called_once'
   create_autospec(..., spec_set=True): m.extra = 1 -> AttributeError: Mock object has no attribute 'extra'

Та же таблица коротко — каждая клетка из строки прогона выше:

что проверяемMock()Mock(spec=PaymentClient)Mock(spec_set=PaymentClient)create_autospec(PaymentClient, instance=True)
существование атрибута: m.chrageпринятоAttributeErrorAttributeErrorAttributeError
присваивание нового атрибута: m.extra = 1принятопринятоAttributeErrorпринято; с spec_set=True — AttributeError
сигнатура вызова: m.charge(100) без currencyпринятопринятопринятоTypeError
опечатка в проверке: m.was_called_once()принятоAttributeErrorAttributeErrorAttributeError

Отсюда точные слова: spec и spec_set ограничивают имена, а неверное число аргументов ловит только autospec — create_autospec или patch(..., autospec=True).

Механизм 5: опечатка в имени проверки

контракт языкаПравило об именах — строка документации Mock; вывод — прогон bench/testing/basics.py, раздел 3. Строка called_once на 3.11 другая — см. историю версий.

Раз Mock создаёт атрибут на любое имя, опечатка в методе-проверке превращается в обычный вызов, который ничего не проверяет. Mock ловит часть опечаток сам — в прогоне mock не вызывали ни разу, так что настоящая проверка должна упасть. Вывод bench/testing/basics.py, раздел 3:

3) опечатка в имени проверки (mock ни разу не вызывали)
   m.assert_called_once()  -> AssertionError: Expected 'mock' to have been called once. Called 0 times.
   m.asert_called_once()   -> AttributeError: 'asert_called_once' is not a valid assertion. Use a spec for the mock if 'asert_called_once' is meant to be an attribute.
   m.assret_called_once()  -> AttributeError: 'assret_called_once' is not a valid assertion. Use a spec for the mock if 'assret_called_once' is meant to be an attribute.
   m.called_once()         -> AttributeError: 'called_once' is not a valid assertion. Use a spec for the mock if 'called_once' is meant to be an attribute.
   m.was_called_once()     -> прошло молча — проверки не было

Имена, начинающиеся с assert, assret, asert, aseert и assrt, Mock не создаёт. С 3.12 к ним добавились имена проверок без префикса: e.g. accessing called_once instead of assert_called_once (например, called_once вместо assert_called_once). На 3.11 строка m.called_once() в том же прогоне — «прошло молча». А m.was_called_once() не ловится ни на одной версии: это просто новый атрибут mock. Защита та же, что в механизме 4, — спецификация: у mock с spec или autospec несуществующих атрибутов нет.

Полный пример: результат — главное, вызов mock — дополнительно

контракт языкаФайлы bench/testing/checkout.py и bench/testing/test_checkout.py; вывод — прогон bench/testing/checkout_run.py, CPython 3.13.7 и pytest 8.4.2, linux/amd64. Скрипт копирует оба файла в пустой каталог и запускает там python -m pytest; флаги после -v только сокращают вывод.

Два файла — их можно скопировать в пустой каталог и запустить. Рабочий код, полный файл bench/testing/checkout.py:

PYTHON
"""Код под тестом для test_checkout.py: оформление заказа с оплатой.
 
Полный пример урока «Тестирование» — два файла, checkout.py и
test_checkout.py. Скопируйте оба в пустой каталог и запустите:
 
    python -m pytest -v
 
Нужен pytest (в записи прогона — 8.4.2). Как дефект в этом файле делает
тест красным — checkout_run.py и runs/checkout_run.txt.
"""
 
 
class PaymentClient:
    """Клиент платёжного сервиса; в тестах его заменяет mock."""
 
    def charge(self, amount, currency):
        raise RuntimeError("в тесте настоящий платёж не нужен")
 
 
def checkout(client, prices, discount=0):
    total = sum(prices) - discount
    client.charge(total, "RUB")
    return total

Тесты, полный файл bench/testing/test_checkout.py:

PYTHON
"""Тесты к checkout.py — полный пример урока «Тестирование».
 
Запуск из каталога с обоими файлами:  python -m pytest -v
"""
from unittest.mock import create_autospec
 
import pytest
 
from checkout import PaymentClient, checkout
 
 
@pytest.fixture
def client():
    # Mock по настоящему классу: несуществующий метод — AttributeError,
    # вызов charge с неверными аргументами — TypeError.
    return create_autospec(PaymentClient, instance=True)
 
 
def test_checkout_returns_total(client):
    total = checkout(client, [100, 200])
    assert total == 300  # главное: результат рабочего кода
    client.charge.assert_called_once_with(300, "RUB")  # дополнительно: взаимодействие
 
 
@pytest.mark.parametrize(
    "prices, discount, expected",
    [([100, 200], 0, 300), ([100], 30, 70), ([], 0, 0)],
)
def test_checkout_discount(client, prices, discount, expected):
    assert checkout(client, prices, discount) == expected

Запуск — python -m pytest -v в каталоге с обоими файлами. Вывод bench/testing/checkout_run.py:

0) как есть: зелёный прогон
   $ python -m pytest -v --tb=line -p no:cacheprovider --no-header -rN
   test_checkout.py::test_checkout_returns_total PASSED
   test_checkout.py::test_checkout_discount[prices0-0-300] PASSED
   test_checkout.py::test_checkout_discount[prices1-30-70] PASSED
   test_checkout.py::test_checkout_discount[prices2-0-0] PASSED
   ============================== 4 passed in <время> ===============================
   код возврата: 0

Порядок утверждений в test_checkout_returns_total не случаен. Первое — assert total == 300 — проверяет результат рабочего кода: ради него тест и пишется. Второе — assert_called_once_with(300, "RUB") — дополнительное утверждение о взаимодействии: код действительно обратился к платёжному клиенту, и с какими аргументами. Тест, который проверяет только вызовы mock, повторяет реализацию, а не проверяет поведение.

Тест полезен, только если дефект в коде делает его красным. Скрипт портит копию bench/testing/checkout.py и запускает те же тесты:

A) дефект в checkout: скидка прибавляется
   правка в checkout.py: total = sum(prices) - discount  =>  total = sum(prices) + discount
   $ python -m pytest -v --tb=line -p no:cacheprovider --no-header -rN
   test_checkout.py::test_checkout_returns_total PASSED
   test_checkout.py::test_checkout_discount[prices0-0-300] PASSED
   test_checkout.py::test_checkout_discount[prices1-30-70] FAILED
   test_checkout.py::test_checkout_discount[prices2-0-0] PASSED
   <каталог>/test_checkout.py:30: AssertionError: assert 130 == 70
   ========================= 1 failed, 3 passed in <время> ==========================
   код возврата: 1

Второй дефект — client.charge(total) без валюты. С фикстурой на create_autospec красными стали все четыре теста: каждый вызов charge ответил TypeError: missing a required argument: 'currency'. А вот тот же дефект с фикстурой на Mock(spec=PaymentClient):

C) тот же дефект, фикстура на Mock(spec=PaymentClient)
   правка в checkout.py: client.charge(total, "RUB")  =>  client.charge(total)
   правка в test_checkout.py: return create_autospec(PaymentClient, instance=True)  =>  return Mock(spec=PaymentClient)
   правка в test_checkout.py: from unittest.mock import create_autospec  =>  from unittest.mock import Mock, create_autospec
   $ python -m pytest -v --tb=line -p no:cacheprovider --no-header -rN
   test_checkout.py::test_checkout_returns_total FAILED
   test_checkout.py::test_checkout_discount[prices0-0-300] PASSED
   test_checkout.py::test_checkout_discount[prices1-30-70] PASSED
   test_checkout.py::test_checkout_discount[prices2-0-0] PASSED
   <stdlib>/unittest/mock.py:979: AssertionError: expected call not found.
   ========================= 1 failed, 3 passed in <время> ==========================
   код возврата: 1

Три теста, смотрящие только на результат, остались зелёными: spec сигнатуру не проверяет, и вызов без валюты прошёл. Дефект поймала лишь дополнительная проверка вызова. Поэтому для зависимостей с сигнатурой — create_autospec.

Глубже: порядок тестов, setUp и pytest

контракт языкаПорядок — Lib/unittest/loader.py; вывод — прогон bench/testing/basics.py, раздел 6, одинаковый на CPython 3.11–3.14. Вывод pytest — bench/testing/pytest_run.py, CPython 3.13.7 и pytest 8.4.2.

Вывод bench/testing/basics.py, раздел 6:

6) порядок тестов и подготовка
   объявлены: test_zebra, test_apple, test_mango
   выполнены: ['test_apple', 'test_mango', 'test_zebra']
   setUpClass: 1 | setUp: 3

unittest сортирует имена методов как строки, поэтому порядок — алфавитный, а не порядок объявления. Точнее, это сравнение строк: test_10 окажется раньше test_2. Тест, который рассчитывает, что другой тест выполнится раньше, — ошибка: тесты должны быть независимы. setUp готовит окружение перед каждым тестом, setUpClass — один раз на класс; поэтому дорогую подготовку, которую тесты не меняют, выносят в setUpClass, а изменяемое состояние — в setUp, чтобы тесты не влияли друг на друга.

pytest делает то же короче: обычный assert, фикстуры вместо setUp, parametrize вместо циклов. Полный файл bench/testing/pytest_examples.py — четыре теста в нём падают нарочно, ради текста, который печатает pytest:

PYTHON
"""Примеры тестов pytest для урока «Тестирование».
 
Файл назван не test_*.py нарочно: pytest не подхватит его сам, а четыре теста
здесь падают специально — ради текста, который pytest печатает при падении.
Запускает его pytest_run.py.
"""
import pytest
 
 
def add(a, b):
    return a + b
 
 
# ── assert без методов: pytest сам раскладывает выражение ────────────────
def test_plain_assert():
    result = add(2, 2)
    assert result == 5
 
 
# ── parametrize: один тест — несколько наборов данных ────────────────────
@pytest.mark.parametrize(
    "a, b, expected",
    [(1, 2, 3), (0, 0, 0), (-1, 1, 0), (2, 2, 5)],
)
def test_add_cases(a, b, expected):
    assert add(a, b) == expected
 
 
# ── фикстура: подготовка и уборка вокруг теста ───────────────────────────
@pytest.fixture
def resource():
    print("\n   [фикстура] подготовка")
    yield {"connected": True}
    print("   [фикстура] уборка")
 
 
def test_uses_fixture(resource):
    assert resource["connected"]
 
 
# ── pytest.raises: ожидаемое исключение — это успех ──────────────────────
def test_raises():
    with pytest.raises(TypeError):
        add(1, "1")
 
 
# ── исключение в теле теста и сбой фикстуры: FAILED против ERROR ────────
def test_type_error():
    add(1, "1")
 
 
@pytest.fixture
def broken():
    raise RuntimeError("фикстура не поднялась")
 
 
def test_with_broken_fixture(broken):
    assert broken

Файл назван не test_*.py, поэтому pytest находит его, только если имя передать явно: python -m pytest bench/testing/pytest_examples.py -v -s (-s — чтобы был виден print из фикстуры). Так его запускает bench/testing/pytest_run.py. Фрагмент вывода — тест с упавшим assert, четыре набора одного parametrize и тест с фикстурой; строки про test_raises и итог опущены:

pytest_examples.py::test_plain_assert FAILED
pytest_examples.py::test_add_cases[1-2-3] PASSED
pytest_examples.py::test_add_cases[0-0-0] PASSED
pytest_examples.py::test_add_cases[-1-1-0] PASSED
pytest_examples.py::test_add_cases[2-2-5] FAILED
pytest_examples.py::test_uses_fixture 
   [фикстура] подготовка
PASSED   [фикстура] уборка

Каждый набор parametrize — отдельный тест со своим именем и своим исходом. Фикстура с yield делает подготовку до теста и уборку после — как setUp и tearDown в одной функции. Порядок здесь — порядок объявления в файле: pytest не сортирует тесты, как unittest. А на упавшем assert pytest сам раскладывает выражение — без assertEqual; вот разбор набора [2-2-5]:

>       assert add(a, b) == expected
E       assert 4 == 5
E        +  where 4 = add(2, 2)

И два последних теста того же прогона — исключение в теле теста и фикстура, которая сама упала при подготовке:

pytest_examples.py::test_type_error FAILED
pytest_examples.py::test_with_broken_fixture ERROR

В pytest исключение в теле — FAILED, как и упавший assert. ERROR — когда тест не смог начаться или закончиться: сломалась фикстура.

Как отвечать на собеседовании

Короткий ответ: тест вызывает код и проверяет результат; в unittest FAIL — сработавшая проверка (AssertionError), ERROR — любое другое исключение. Внешние зависимости подменяют Mock через patch — в том модуле, где имя используется. Главное утверждение теста — о результате кода; проверка вызова mock — дополнительная. Моки создают по настоящему объекту: Mock(spec=…) отвергает несуществующие имена, а неверный вызов ловит только create_autospec или autospec=True. Тесты независимы: на порядок их выполнения не опираются, состояние готовится в setUp или фикстуре.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

На «я замокал функцию, а тест ходит в настоящий сервис». «Скорее всего патч не там: модуль сделал from x import f, а патчили x.f. У меня patch('time.time') не подменил время в модуле с from time import time, а patch('app_from.time') — подменил».

На «чем опасен Mock». «Он соглашается на всё. В прогоне голый Mock принял вызов без обязательного аргумента и метод с опечаткой. С create_autospec первое — TypeError, второе — AttributeError. А Mock(spec=…) ловит только второе: вызов charge(100) без валюты он принял — spec ограничивает имена, а не сигнатуру».

На «чем pytest лучше unittest». «Меньше кода: обычный assert, который pytest сам раскладывает при падении, фикстуры с yield для подготовки и уборки, parametrize — каждый набор данных отдельным тестом».

На «в каком порядке выполняются тесты». «В unittest — по алфавиту имён методов, у меня test_apple раньше test_zebra, хотя объявлен позже. Опираться на порядок нельзя — тесты должны быть независимы».

Дальше спросят

Спросят дальше

Чем return_value отличается от side_effect?

Короткий ответ

return_value — одно значение на все вызовы. side_effect — поведение на каждый вызов: список значений выдаётся по очереди, исключение — бросается, функция — вызывается с теми же аргументами. В прогоне side_effect=[10, 20, TimeoutError(…)] дал 10, потом 20, потом исключение.

Спросят дальше

Когда моки — это плохо?

Короткий ответ

Когда тест проверяет моки, а не код: всё подменено, и тест повторяет реализацию. Такой тест проходит при сломанном коде и падает при правильном рефакторинге. Подменяют границы — сеть, время, диск, — а не внутренние функции собственного кода.

Спросят дальше

Чем фикстура pytest отличается от setUp?

Короткий ответ

Фикстуру тест запрашивает по имени параметра, и она может готовить и убирать в одной функции: всё до yield — подготовка, после — уборка. В выводе pytest «подготовка» напечаталась до теста, «уборка» — после. Фикстуры можно комбинировать и задавать им область: на тест, модуль или весь запуск.

Частые заблуждения

Утверждение

patch('time.time') подменяет время во всём коде

На самом деле

Только там, где функцию ищут через модуль time. Модуль, сделавший from time import time, держит свою ссылку: в прогоне его now() вернул не 42.0 — функция осталась настоящей.

Утверждение

Если тест с моком прошёл, код вызван правильно

На самом деле

Голый Mock принимает любые аргументы и любые имена: Mock().charge(100) без валюты и Mock().chrage(…) с опечаткой прошли. Имена проверяет уже Mock(spec=…), аргументы — только create_autospec или autospec=True.

Утверждение

Mock(spec=Класс) проверяет, с какими аргументами вызван метод

На самом деле

Нет: spec ограничивает имена атрибутов, сигнатуру он не смотрит. В прогоне Mock(spec=PaymentClient).charge(100) без валюты — «принято», а create_autospec ответил TypeError: missing a required argument: 'currency'.

Утверждение

Опечатку в assert-методе Mock всегда поймает

На самом деле

Только для имён с префиксами вроде assert и asert и, с 3.12, имён проверок без префикса. m.was_called_once() прошёл молча на всех версиях.

Утверждение

В unittest тесты выполняются в порядке объявления

На самом деле

По алфавиту имён: объявленные test_zebra, test_apple, test_mango выполнились как apple, mango, zebra. pytest по умолчанию идёт в порядке файла — в прогоне test_plain_assert раньше test_add_cases.

Утверждение

FAIL и ERROR — одно и то же

На самом деле

В unittest FAIL — сработавшая проверка, AssertionError; ERROR — любое другое исключение. Он считает их отдельно.

История версий

ВерсияИзменениеЧто это значит для кода
3.11Опорная точка урока; прогоны — CPython 3.11.15, 3.12.3, 3.13.7, 3.14.7 на linux/amd64, pytest 8.4.2 на 3.13.7. Mock не создаёт атрибуты с префиксами assert, assret, asert, aseert, assrt; m.called_once() проходит молча.
3.12Обращение к имени проверки без префикса assert_ — called_once и подобным — тоже AttributeError. Тест с такой опечаткой, молча проходивший на 3.11, на 3.12 начнёт падать — и это правильно.
3.15Вышел 9 октября 2026. Урок на нём не запускался: граница проверки — CPython 3.14.7, сверено 14.10.2026. Таблица spec/spec_set/autospec одинакова на 3.11–3.14; на 3.15 её перепроверяют тем же разделом 7 скрипта.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Mock с return_value вызвали дважды с разными аргументами. Что напечатает этот код?
from unittest.mock import Mock

m = Mock(return_value=10)
print(m(1), m(2), m.call_count)
print(m.call_args)

Практика · оцените

В TestCase пять тестов, есть setUp и setUpClass. Сколько раз вызовется setUp?
раз

Проверьте себя

Вопрос 1 из 4

Модуль report делает from api import fetch. Как подменить fetch в тестах report?

Чем измерено

Наблюдения этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём запускали и что получилось.

Источники и что читать дальше

3 ИСТОЧНИКА

  1. unittest.mock — строки документации Mock и create_autospecОфициальная документация. Какие имена Mock не создаёт сам: «By default, accessing any attribute whose name starts with *assert*, *assret*, *asert*, *aseert*, or *assrt* raises an AttributeError. Additionally, an AttributeError is raised when accessing attributes that match the name of an assertion method without the prefix assert_, e.g. accessing called_once instead of assert_called_once» (По умолчанию обращение к любому атрибуту, имя которого начинается с assert, assret, asert, aseert или assrt, вызывает AttributeError. Кроме того, AttributeError вызывает обращение к атрибуту, совпадающему с именем метода-проверки без префикса assert_, например called_once вместо assert_called_once). Второе предложение есть в строке документации Mock на 3.12, 3.13 и 3.14 и отсутствует на 3.11. Что даёт spec: «Accessing any attribute not in this list will raise an AttributeError» (Обращение к любому атрибуту не из этого списка вызывает AttributeError), а spec_set: «A stricter variant of spec. If used, attempting to *set* or get an attribute on the mock that isn't on the object passed as spec_set will raise an AttributeError» (Более строгий вариант spec. С ним попытка задать или прочитать у mock атрибут, которого нет у объекта, переданного как spec_set, вызывает AttributeError). О сигнатуре вызова в описании spec не сказано ничего. И create_autospec: «Functions or methods being mocked will have their arguments checked to check that they are called with the correct signature» (У подменяемых функций и методов проверяются аргументы — вызваны ли они с правильной сигнатурой). Сверено вызовами Mock.__doc__ и create_autospec.__doc__ на 3.11.15, 3.12.3, 3.13.7 и 3.14.7.https://docs.python.org/3.13/library/unittest.mock.html
  2. unittest.mock.patch — строка документации, autospecОфициальная документация. Что даёт patch(..., autospec=True): «If you set autospec=True then the mock will be created with a spec from the object being replaced» (Если задать autospec=True, mock создаётся со спецификацией подменяемого объекта), и у подменённых методов и функций «will have their arguments checked and will raise a TypeError if they are called with the wrong signature» (проверяются аргументы, и при вызове с неверной сигнатурой будет TypeError). Сверено вызовом mock.patch.__doc__ на 3.11.15, 3.12.3, 3.13.7 и 3.14.7.https://docs.python.org/3.13/library/unittest.mock.html#autospeccing
  3. Lib/unittest/loader.py — порядок методов тестаИсходный код CPython. Строка 69 на 3.12.3 и 3.13.7: sortTestMethodsUsing = staticmethod(util.three_way_cmp) — загрузчик сортирует имена методов теста сравнением строк, отсюда алфавитный порядок. Строки документации TestCase: setUp — «Hook method for setting up the test fixture before exercising it» (Метод-перехватчик для подготовки окружения теста перед его выполнением), setUpClass — «Hook method for setting up class fixture before running tests in the class» (Метод-перехватчик для подготовки окружения класса перед запуском его тестов).https://github.com/python/cpython/blob/v3.13.7/Lib/unittest/loader.py