Read the book: «Тестируем Python. 12 лабораторных работ с pytest»
Проверка должна замечать ошибку
От правила к проверке
Представьте две реализации одного правила. Одна ведёт себя правильно, другая ошибается только на границе. Если ваш тест принимает обе, зелёный результат почти ничего не сообщает именно об этой ошибке. Поэтому каждая работа начинается с точного условия, а намеренно испорченная версия нужна как контроль чувствительности теста. Мы не станем исправлять ожидаемый ответ под существующий код: сначала выясним, какой ответ требует условие.
Здесь нет одного большого приложения, которое нужно собрать, прежде чем увидеть первое падение. Каждая работа живёт в собственной папке и содержит небольшой файл logic.py с проверяемой логикой и файл test_logic.py с тестами. После разбора вы полностью замените учебный logic.py исправленной версией. Одноимённые файлы из разных работ не нужно объединять.
Книга рассчитана на читателя, который уже пишет функции, понимает импорт модулей, списки, словари и простые классы. Это следующий шаг от ручного вызова функции к воспроизводимой проверке её обещаний. Основы синтаксиса Python здесь не пересказываются. Стабы и наблюдатели будут небольшими функциями и вызываемыми объектами: механизм важнее большого набора инструментов.
Все ситуации и данные вымышлены. Книга подготовлена с помощью ИИ и прошла редакторскую проверку; исходные тесты и код, извлечённый из FB2, проверяются запуском в отдельных временных каталогах. Такая проверка охватывает конечный набор примеров, а не все возможные входы. Она не делает учебные реализации готовыми компонентами рабочего проекта.
Подготовка без рабочего проекта
Используйте отдельную новую папку, например pytest-practice. Не открывайте для этих опытов репозиторий работодателя, каталог с личными документами или существующее окружение проекта. Нужен установленный Python 3.12 и pytest версии 8.3.5. Эта версия выбрана для воспроизводимости примеров; мы не утверждаем, что она самая новая. Проверка книги выполнялась на Python 3.12.14.
Сначала проверьте установленный интерпретатор: python3.12 --version должен показать Python 3.12.x. В терминале новой папки создайте локальное окружение командой python3.12 -m venv .venv. Если интерпретатор установлен под другим именем, используйте его проверенный путь, а не случайную системную версию. В Windows аналогичная команда — py -3.12 -m venv .venv. В macOS/Linux активируйте окружение командой source .venv/bin/activate; в Windows PowerShell — .venv\Scripts\Activate.ps1. Если политика PowerShell запрещает активацию, не меняйте её ради книги: обращайтесь к .venv\Scripts\python.exe напрямую во всех следующих командах вместо python. Для macOS/Linux аналогичный прямой путь — .venv/bin/python.
После активации выполните python -m pip install pytest==8.3.5, затем python --version и python -m pytest --version. Установка загружает бесплатный пакет и его зависимости из настроенного у pip источника; интернет нужен на этом подготовительном шаге. Сами лабораторные работы не обращаются в сеть. Если Python или pip не найден, исправьте выбор интерпретатора до работы с примерами — это не ошибка тестируемой функции.
Внутри pytest-practice создайте через файловый менеджер или редактор отдельные папки lab01, lab02 и далее. Для выбранной работы откройте терминал именно в её папке. Не создавайте файлы с именами pytest.py, pathlib.py или datetime.py: они могут затенить библиотечные модули. Сохраняйте наши файлы в UTF-8, а не в формате текстового процессора. Один файл logic.py относится только к одной работе.
Как перенести код из читалки
В основных листингах каждая строка начинается с номера и разделителя, например 001│. Все обычные пробелы справа от черты показаны точками ·, чтобы читалка не скрыла отступы. Это печатная разметка, не Python. Для ручного восстановления удалите номер и черту, затем замените каждую точку · обычным пробелом, в том числе внутри строковых литералов. Точка . остаётся точкой. В исходных примерах символа · нет. Строка с одним лишь префиксом означает пустую строку кода.
Ниже дан небольшой вспомогательный скрипт без обязательных отступов и без служебных номеров. Сохраните его как restore_listing.py в отдельной учебной папке. Скопируйте ровно один номерованный листинг в copied.txt, сохранив UTF-8, и выполните python restore_listing.py. Скрипт проверяет последовательность номеров и создаёт новый restored.py. Он не запускает полученный код и откажется перезаписывать существующий restored.py. Просмотрите результат в редакторе, затем перенесите его под указанным именем logic.py, test_logic.py или test_extra.py в папку выбранной работы. Перед следующим преобразованием перенесите предыдущий restored.py, не затирая нужные файлы.
Если читалка добавила абзацные пробелы до номера, скрипт их уберёт. Визуальный перенос по ширине не является новой строкой кода. Если при копировании он превратился в настоящий разрыв, соедините части до следующего номера вручную: скрипт должен остановиться, а не угадывать. Не меняйте прямые кавычки на типографские и не применяйте общее обрезание отступов к уже восстановленному Python. Во вспомогательном скрипте все строки начинаются с левого края; удалите только добавленный читалкой начальный отступ.
Сначала создайте logic.py из блока «Версия с намеренной ошибкой», затем test_logic.py из следующего блока. Команда python -m pytest -q test_logic.py запускает только основной набор этой работы. Часть тестов должна упасть: ниже указано ожидаемое количество. Не переносите текст ожидаемого результата или номера строк в Python-файлы. Если появляется SyntaxError, IndentationError, ImportError или сообщение об ошибке сбора тестов, ещё рано обсуждать поведение функции — сначала проверьте копирование, имена файлов и текущую папку.
После разбора замените содержимое того же учебного logic.py блоком «Исправленная версия» и повторите ту же команду новым запуском. Все основные тесты должны пройти. Затем решите упражнение, не меняя исправленную реализацию. Ответ содержит отдельный test_extra.py: положите его рядом и выполните python -m pytest -q test_logic.py test_extra.py. Это намеренно явное перечисление файлов, чтобы случайные тесты из соседних работ не участвовали.
restore_listing.py — вспомогательный скрипт без номеров
from pathlib import Path
raw = Path("copied.txt").read_text(encoding="utf-8")
rows = [s.strip() for s in raw.splitlines() if s.strip()]
assert rows and all("│" in s for s in rows)
parts = [s.split("│", 1) for s in rows]
assert [p[0] for p in parts] == [f"{i:03d}" for i in range(1, len(parts) + 1)]
code = "\n".join(p[1].replace("·", " ") for p in parts) + "\n"
out = open("restored.py", "x", encoding="utf-8")
out.write(code)
out.close()
Как читать красный и зелёный результат
Основной тест обычно состоит из подготовки данных, одного действия и проверки наблюдаемого результата. Ожидаемое значение берётся из контракта. Вычислять его тем же алгоритмом, который проверяется, опасно: одна и та же ошибка может оказаться по обе стороны сравнения. Короткие конкретные значения легче проверить глазами, а параметризация позволяет повторить одно правило на нескольких заранее выбранных входах.
В отчёте pytest слово FAILED указывает на неуспешный тест, а строка assert помогает увидеть, какие значения сравнивались. ERROR, особенно во время collection, означает иной тип проблемы: например, файл не удалось импортировать или не подготовилась фикстура. Намеренные дефекты этой книги должны давать именно ожидаемые провалы проверок, не аварии сбора. Время выполнения, абсолютные пути и оформление отчёта у вас будут другими.
Строка passed означает только, что собранные проверки прошли. Она не говорит, что тестов достаточно, что все важные входы рассмотрены или что внешний сервис работает. Даже маленький набор можно улучшить, задав вопрос: какую реалистичную неправильную реализацию он пока пропустит? Для каждого упражнения мы назовём такой пробел и его границы, а не будем обещать полную защиту от ошибок.
Часть работ проверяет только значения и состояние в памяти; работа с tmp_path действительно использует локальную файловую систему. Это небольшая проверка границы с файлом, а не доказательство поведения всех дисков и операционных систем. Подмена функции даты или справочника делает тест воспроизводимым, но не проверяет настоящий источник времени, удалённую базу или доставку сообщений. Для них нужны отдельные проверки вне этого практикума.
Выбрать лабораторную работу
1. Последнее место: первое утверждение о границе
2. Таблица границ: один тест, семь самостоятельных примеров
3. Отказ тоже результат: проверяем нужное исключение
4. Верный ответ, испорченный список: наблюдаем побочный эффект
5. Две очереди, одна заготовка: свежие fixture и владение данными
6. Допуск в миллиметрах: точность требования, а не красота числа
7. Временный файл: последний перевод строки тоже данные
8. Stub: известный ноль и неизвестная очередь
9. Spy: уведомление только после проверки
10. Monkeypatch: календарная граница без настоящих часов
11. Регрессия: пустые данные не означают отсутствующие
12. Переход состояния: отказ не должен оставлять следов
Лабораторные работы
Работа 1. Последнее место: первое утверждение о границе
Условие и выбор проверок
В вымышленной мастерской нужно ответить, можно ли принять ещё одного участника. Функция ничего не бронирует: она получает два числа и возвращает решение. На этой маленькой задаче научимся превращать словесное правило в проверяемое утверждение и выбирать пример, который различает правильную и почти правильную формулы.
Контракт учебной функции
1. can_join(capacity, occupied) принимает ровно встроенные int; bool и любые другие типы вызывают TypeError.
2. capacity не меньше 1; occupied находится от 0 до capacity включительно. Нарушение этих границ вызывает ValueError.
3. Результат — bool: True, только когда занятых мест строго меньше вместимости; при полностью заполненной мастерской — False.
4. Функция не меняет внешнее состояние и не записывает участника. Тексты сообщений об ошибках не являются частью контракта.
Как выбрать проверки
Начнём не с оператора сравнения, а с вопроса к правилу. Если мастерская рассчитана на четыре человека и пришли трое, ответ положительный. Если пришли четверо, свободных мест уже нет. Обе ситуации допустимы как вход функции: полная мастерская не является ошибкой данных. Важно отделить обычный отрицательный ответ от исключения, означающего, например, невозможное число занятых мест.
Выберем три ситуации: никого нет, осталось одно место, свободных мест нет. Первая проверяет понятную исходную точку, вторая — ближайшее допустимое состояние перед заполнением, третья — саму границу. Десять проверок с полупустыми мастерскими могли бы дать меньше полезной информации: неверное сравнение способно соглашаться с правильным на всех таких примерах.
В test_logic.py каждая функция описывает одну ситуацию. Вызов can_join — действие, а assert фиксирует ожидаемый результат. Используем is True и is False, поскольку контракт обещает именно логическое значение: просто истинный список или число 1 не должны подменять True. Имена тестов называют поведение, поэтому по имени упавшей проверки легче понять, какое правило нарушено.
Ожидаемые значения записаны буквально. Не следует получать ожидаемый ответ выражением occupied <= capacity: так тест скопирует спорное решение из программы. Здесь независимым основанием служит описанная ситуация: четыре занятых места из четырёх означают запрет на нового участника. Тест и реализация должны встречаться в требовании, а не подтверждать друг друга одинаковым кодом.
У дефектной версии есть полезная особенность: два теста проходят. Это показывает, почему успешный запуск на одном удобном примере ещё не характеризует всю функцию. В отчёте нас интересует сравнение фактического True с ожидаемым False у полностью заполненной мастерской. Остальные две проверки удерживают положительную сторону правила и не позволяют исправить падение возвратом False для всех входов.
После замены реализации запускаем тот же файл тестов, не ослабляя утверждение. Меняется одна граница: равенство перестаёт означать разрешение. Пока мы не проверяем все ошибочные входы, хотя их обработка определена контрактом и присутствует в коде. Это осознанный фокус первой лабораторной: научиться обнаруживать конкретный дефект, не выдавая три примера за исчерпывающую проверку.