Тестирование: 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 — проверяемая функция, она определена в другом месте. Полный
запускаемый пример — ниже, в разделе «Полный пример».
# 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 — четыре теста 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:
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 — подменять там, где имя ищут
Два модуля возвращают текущее время. Первый пишет 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 проверяет вызов
У 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).
AttributeErrorspec_set — A stricter variant of
(более строгий вариант spec): запрещает
ещё и присваивать новые имена. О сигнатуре вызова в описании обоих не
сказано ничего — её проверяет только speccreate_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 | принято | AttributeError | AttributeError | AttributeError |
присваивание нового атрибута: m.extra = 1 | принято | принято | AttributeError | принято; с spec_set=True — AttributeError |
сигнатура вызова: m.charge(100) без currency | принято | принято | принято | TypeError |
опечатка в проверке: m.was_called_once() | принято | AttributeError | AttributeError | AttributeError |
Отсюда точные слова: spec и spec_set ограничивают имена, а неверное
число аргументов ловит только autospec — create_autospec или
patch(..., autospec=True).
Механизм 5: опечатка в имени проверки
Раз 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:
"""Код под тестом для 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:
"""Тесты к 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
Вывод 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:
"""Примеры тестов 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 скрипта. |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
from unittest.mock import Mock m = Mock(return_value=10) print(m(1), m(2), m.call_count) print(m.call_args)
Практика · оцените
Проверьте себя
Модуль report делает from api import fetch. Как подменить fetch в тестах report?
Чем измерено
Наблюдения этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём запускали и что получилось.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Тест — это функция, которая вызывает код и проверяет результат; основных исходов у него три: прошёл, упал на проверке или упал с исключением. В стандартной библиотеке —
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— один раз.
На самом деле
- Только там, где функцию ищут через модуль
time. Модуль, сделавшийfrom time import time, держит свою ссылку: в прогоне егоnow()вернул не 42.0 — функция осталась настоящей. - Голый
Mockпринимает любые аргументы и любые имена:Mock().charge(100)без валюты иMock().chrage(…)с опечаткой прошли. Имена проверяет ужеMock(spec=…), аргументы — толькоcreate_autospecилиautospec=True. - Нет:
specограничивает имена атрибутов, сигнатуру он не смотрит. В прогонеMock(spec=PaymentClient).charge(100)без валюты — «принято», аcreate_autospecответилTypeError: missing a required argument: 'currency'. - Только для имён с префиксами вроде
assertиasertи, с 3.12, имён проверок без префикса.m.was_called_once()прошёл молча на всех версиях. - По алфавиту имён: объявленные
test_zebra,test_apple,test_mangoвыполнились как apple, mango, zebra. pytest по умолчанию идёт в порядке файла — в прогонеtest_plain_assertраньшеtest_add_cases. - В
unittestFAIL — сработавшая проверка,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 скрипта.<
Что разобрано
- База: что такое тест и чем unittest отличается от pytest
- Механизм 1: прошёл, FAIL и ERROR
- Механизм 2: Mock — объект, который соглашается на всё
- Механизм 3: patch — подменять там, где имя ищут
- Механизм 4: спецификация — spec ограничивает имена, autospec проверяет вызов
- Механизм 5: опечатка в имени проверки
- Полный пример: результат — главное, вызов mock — дополнительно
- Глубже: порядок тестов, setUp и pytest
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
3 ИСТОЧНИКА
- 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. accessingcalled_onceinstead ofassert_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 anAttributeError» (Обращение к любому атрибуту не из этого списка вызывает AttributeError), а spec_set: «A stricter variant ofspec. If used, attempting to *set* or get an attribute on the mock that isn't on the object passed asspec_setwill raise anAttributeError» (Более строгий вариант 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 - unittest.mock.patch — строка документации, autospecОфициальная документация. Что даёт patch(..., autospec=True): «If you set
autospec=Truethen the mock will be created with a spec from the object being replaced» (Если задать autospec=True, mock создаётся со спецификацией подменяемого объекта), и у подменённых методов и функций «will have their arguments checked and will raise aTypeErrorif 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 - 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