ЗАМЕР
bench/testing/checkout_run.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/unit-testing
- Как запустить
python3.11 bench/testing/basics.py python3.12 bench/testing/basics.py python3.13 bench/testing/basics.py python3.14 bench/testing/basics.py python3.13 -m venv .venv-pytest .venv-pytest/bin/pip install pytest==8.4.2 .venv-pytest/bin/python bench/testing/pytest_run.py .venv-pytest/bin/python bench/testing/checkout_run.py python3.11 bench/testing/practice.py python3.12 bench/testing/practice.py python3.13 bench/testing/practice.py python3.14 bench/testing/practice.py
Запись прогона
Замеры для урока «Тестирование»
| Скрипт | Что делает |
|---|---|
basics.py |
наблюдения на стандартной библиотеке: FAIL против ERROR, Mock и его поля, опечатки в именах проверок, где патчить, Mock против create_autospec, порядок тестов и setUp, таблица Mock / spec / spec_set / autospec (раздел 7) |
pytest_run.py |
запускает pytest_examples.py под pytest и печатает вывод без времени и путей: разложенный assert, parametrize, фикстура с yield, pytest.raises, исключение в теле теста (FAILED) и упавшая фикстура (ERROR) |
pytest_examples.py |
примеры тестов pytest; четыре не проходят нарочно — три FAILED и один ERROR. Назван не test_*.py, чтобы pytest не подхватывал его сам |
checkout.py, test_checkout.py |
полный зелёный пример урока: рабочий код и тесты к нему (результат — главное утверждение, вызов mock — дополнительное; parametrize, фикстура). Запуск — python -m pytest -v в каталоге с обоими файлами |
checkout_run.py |
копирует оба файла в пустой каталог, запускает python -m pytest, затем портит копию тремя способами и запускает снова |
practice.py |
источник ответов двух практических задач урока |
python3.11 bench/testing/basics.py
python3.12 bench/testing/basics.py
python3.13 bench/testing/basics.py
python3.14 bench/testing/basics.py
python3.13 -m venv .venv-pytest
.venv-pytest/bin/pip install pytest==8.4.2
.venv-pytest/bin/python bench/testing/pytest_run.py
.venv-pytest/bin/python bench/testing/checkout_run.py
python3.11 bench/testing/practice.py
python3.12 bench/testing/practice.py
python3.13 bench/testing/practice.py
python3.14 bench/testing/practice.py
Записи в runs/: без суффикса — CPython 3.13.7, остальные — с суффиксом
версии (3.11.15, 3.12.3, 3.14.7); pytest_run.txt и checkout_run.txt —
CPython 3.13.7 и pytest 8.4.2; всё — linux/amd64. basics*.txt перезапущены
14.10.2026 после добавления раздела 7 — разделы 1–6 не изменились.
Что получилось
FAIL против ERROR: assertEqual(add(2, 2), 5) — FAIL, голый
assert — тоже FAIL (AssertionError), add(2, "2") — ERROR с
TypeError; итого тестов 4, failures 2, errors 1.
pytest: 9 тестов — 3 failed, 5 passed, 1 error. Исключение в теле теста
(test_type_error) — FAILED, упавшая фикстура (test_with_broken_fixture) —
ERROR; тесты идут в порядке файла.
Практика: Mock(return_value=10), вызванный как m(1) и m(2), —
10 10 2 и call(2); в TestCase с пятью тестами setUp вызван 5 раз.
Опечатки в проверках (mock ни разу не вызывали): asert_called_once и
assret_called_once — AttributeError на всех версиях; called_once — на
3.11 проходит молча, с 3.12 — AttributeError; was_called_once — молча на
всех версиях.
Где патчить: под patch('time.time') модуль с import time видит
подмену, модуль с from time import time — нет; patch('app_from.time')
подменяет.
Спецификация: голый Mock принял charge(100) без валюты и метод
chrage; create_autospec ответил TypeError: missing a required argument: 'currency' и AttributeError.
Порядок: объявлены test_zebra, test_apple, test_mango — выполнены
apple, mango, zebra; setUpClass — 1 раз, setUp — 3.
Остальные строки вывода одинаковы на 3.11–3.14.
Mock / spec / spec_set / autospec (раздел 7, добавлен 14.10.2026 по
внешнему аудиту, замечание PY-TEST-01; одинаково на 3.11–3.14).
Несуществующее имя m.chrage: голый Mock — принято, остальные три —
AttributeError. Присваивание m.extra = 1: AttributeError только у
spec_set (и у create_autospec(..., spec_set=True)). Вызов m.charge(100)
без валюты: Mock(), Mock(spec=…) и Mock(spec_set=…) — принято,
create_autospec — TypeError: missing a required argument: 'currency'.
m.was_called_once(): голый Mock — принято, со спецификацией любого вида —
AttributeError.
Полный пример (checkout_run.txt): как есть — 4 passed. Дефект «скидка
прибавляется» — 1 failed (assert 130 == 70). Дефект «charge без валюты»:
с фикстурой на create_autospec — 4 failed (TypeError), с фикстурой на
Mock(spec=PaymentClient) — 1 failed, 3 passed: тесты, проверяющие только
результат, дефект не видят, его ловит лишь assert_called_once_with.
Скрипт
89 строк"""Полный пример checkout.py + test_checkout.py: зелёный прогон и два дефекта.
Нужен pytest. В записи прогона — CPython 3.13.7 и pytest 8.4.2:
.venv-pytest/bin/python bench/testing/checkout_run.py
Что делает. Копирует checkout.py и test_checkout.py в пустой временный
каталог и запускает там ровно то, что запустил бы читатель, —
`python -m pytest -v`. Затем трижды портит копию и запускает снова:
A. дефект в рабочем коде: скидка прибавляется, а не вычитается;
B. дефект в рабочем коде: charge вызван без валюты — create_autospec
в фикстуре отвечает TypeError;
C. тот же дефект B, но фикстура построена на Mock(spec=PaymentClient):
spec сигнатуру не проверяет, и тесты, которые смотрят только на
результат, остаются зелёными — дефект ловит лишь дополнительная
проверка вызова assert_called_once_with.
Исходные файлы не меняются. Время и пути из вывода убраны; строки с
процентами хода и с разделителями без итога — тоже.
"""
import os
import re
import shutil
import subprocess
import sys
import sysconfig
import tempfile
import pytest
here = os.path.dirname(os.path.abspath(__file__))
# Читатель запускает `python -m pytest -v`; остальные флаги только сокращают
# вывод: разбор падения в одну строку, без заголовка и без кеша pytest.
FLAGS = ["-v", "--tb=line", "-p", "no:cacheprovider", "--no-header", "-rN"]
print("PY", sys.version.split()[0], "| pytest", pytest.__version__)
def run(title, edits):
work = tempfile.mkdtemp()
for name in ("checkout.py", "test_checkout.py"):
shutil.copy(os.path.join(here, name), work)
for name, old, new in edits:
path = os.path.join(work, name)
src = open(path, encoding="utf-8").read()
assert src.count(old) == 1, (name, old)
open(path, "w", encoding="utf-8").write(src.replace(old, new))
print()
print(title)
for name, old, new in edits:
print(f" правка в {name}: {old.strip()} => {new.strip()}")
print(" $ python -m pytest " + " ".join(FLAGS))
out = subprocess.run(
[sys.executable, "-m", "pytest", *FLAGS],
cwd=work,
capture_output=True,
text=True,
env=dict(os.environ, PYTHONDONTWRITEBYTECODE="1"),
)
text = out.stdout.replace(work, "<каталог>").replace(sysconfig.get_paths()["stdlib"], "<stdlib>")
text = re.sub(r" in \d+\.\d+s", " in <время>", text)
text = re.sub(r" +\[ *\d+%\]", "", text)
for line in text.rstrip().splitlines():
if line.startswith(("=", "collecting", "rootdir", "platform", "cachedir", "plugins")) and "passed" not in line and "failed" not in line:
continue
if line.strip():
print(" ", line)
print(" код возврата:", out.returncode)
shutil.rmtree(work)
run("0) как есть: зелёный прогон", [])
run(
"A) дефект в checkout: скидка прибавляется",
[("checkout.py", "total = sum(prices) - discount", "total = sum(prices) + discount")],
)
run(
"B) дефект в checkout: charge без валюты, фикстура на create_autospec",
[("checkout.py", 'client.charge(total, "RUB")', "client.charge(total)")],
)
run(
"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"),
],
)