Pytest — инструмент для запуска тестов Python. Ты задаёшь входные данные и ожидаемый результат, а pytest выполняет проверки и показывает, где результат не совпал. Начнём с функции, которая правильно обрабатывает длинную строку, но ошибается на минимально допустимой длине. Первый тест обнаружит ошибку; затем проверим исправление на пяти примерах.
Для работы достаточно понимать функции, аргументы и return. Примеры запускаются локально из терминала: понадобятся два файла и установленный pytest.
Установить pytest в отдельное окружение
Создай пустую папку проекта и виртуальное окружение. В Linux или macOS с bash/zsh выполни:
mkdir pytest-start
cd pytest-start
python3 -m venv .venv
source .venv/bin/activate
python -m pip install pytest
python -m pytest --version
На Windows сначала создай и активируй окружение по инструкции для venv, затем выполни последние две команды. В проверенной среде команда версии вывела pytest 9.1.1; примеры выполнены на CPython 3.14.7. При установке без указания версии результат со временем может отличаться.
Запись python -m pytest запускает pytest через выбранный интерпретатор Python. Это помогает держать установку пакета и запуск тестов в одном окружении. Такой запуск также добавляет текущую папку в путь поиска модулей; поведение описано в документации pytest.
Ошибка, которую не видно на длинной строке
Зададим учебное правило: функция принимает строку и возвращает True, если её длина не меньше восьми, иначе False. Мы проверяем только длину через len(). Это условие упражнения, а не рекомендация по безопасности паролей.
Сохрани в файле passwords.py эту версию с ошибкой:
def is_long_enough(password: str) -> bool:
return len(password) > 8
Проверь два обычных случая из терминала, оставаясь в папке pytest-start:
python -c "from passwords import is_long_enough; print(is_long_enough('a1b2c3d4e5f6')); print(is_long_enough('abc'))"
True
False
Оба результата верны. Но строку ровно из восьми символов мы ещё не проверяли. Условие > 8 её отвергнет, хотя по правилу она должна пройти. Разница между «больше восьми» и «не меньше восьми» уместилась в один знак.
Ручная проверка тоже могла найти эту ошибку. Польза теста в том, что выбранный пример и ожидание останутся в коде: после следующей правки не придётся вспоминать, что вводить и с чем сравнивать ответ.
Написать первый тест и получить FAILED
Рядом создай test_passwords.py:
from passwords import is_long_enough
def test_accepts_exactly_eight_characters():
assert is_long_enough("abcd1234") is True
Теперь в проекте два файла:
pytest-start/
├── passwords.py
└── test_passwords.py
Каталог .venv на схеме опущен. В тесте есть три существенные части:
from passwords import ...подключает проверяемую функцию из соседнего файла;- имя
test_accepts_exactly_eight_charactersначинается сtest_, поэтому pytest распознаёт функцию как тест; assert ... is Trueтребует именно булев результатTrue, как и обещает наш контракт.
Для первой проверки import pytest не нужен: assert входит в Python. Сам pytest здесь находит и запускает тест, а при провале дополняет сообщение значениями выражений.
python -m pytest -q
В выводе будет 1 failed. Ключевой фрагмент отчёта:
> assert is_long_enough("abcd1234") is True
E AssertionError: assert False is True
Читай его снизу вверх: функция вернула False, проверка ожидала True. Затем смотри строку с assert, чтобы увидеть входные данные. AssertionError здесь означает, что ожидание теста не выполнилось. Подробности такого отчёта разобраны в документации по assert.
Один вход показывает ошибку и проверяет исправление
Исправить условие и сохранить проверку
Замени содержимое passwords.py:
def is_long_enough(password: str) -> bool:
return len(password) >= 8
Снова выполни python -m pytest -q. Теперь итог — 1 passed.
Тест оставь: если при будущей правке в функцию вернётся строгое >, он снова упадёт. Это регрессионная проверка, то есть проверка против возврата уже найденной ошибки.
Один зелёный тест ещё не означает, что функция верна на всех входах. Например, реализация return True тоже прошла бы эту проверку. Значит, нужны случаи, для которых ответ должен быть False.
Проверить несколько случаев через parametrize
Сначала выберем данные. Для порога восемь нужны значения по обе стороны границы, сама граница, пустой ввод и обычная длинная строка:
| Случай | Строка | Длина | Ожидаем | Что проверяем |
|---|---|---|---|---|
| Пустая строка | "" | 0 | False | Отсутствие символов |
| Ниже границы | "abcd123" | 7 | False | Слишком короткая строка не проходит |
| На границе | "abcd1234" | 8 | True | Минимальная длина включена |
| Выше границы | "abcd12345" | 9 | True | Следующее значение тоже проходит |
| Обычная длинная | "a1b2c3d4e5f6" | 12 | True | Рабочий случай вдали от границы |
@pytest.mark.parametrize запускает одну тестовую функцию с разными наборами аргументов. Замени весь файл test_passwords.py следующим кодом. Отдельный первый тест больше не нужен: его вход уже есть в таблице.
import pytest
from passwords import is_long_enough
@pytest.mark.parametrize(
"password, expected",
[
("", False),
("abcd123", False),
("abcd1234", True),
("abcd12345", True),
("a1b2c3d4e5f6", True),
],
ids=["empty", "below", "minimum", "above", "ordinary"],
)
def test_password_length(password, expected):
assert is_long_enough(password) is expected
Первая строка декоратора называет параметры тестовой функции. Каждый кортеж ниже задаёт их значения: сначала password, затем expected. Список ids даёт случаям короткие имена в отчёте. Без него тесты тоже работают, но pytest составит имена сам из параметров.
При запуске python -m pytest -q получишь 5 passed. Это пять отдельных проверок: pytest покажет результат каждой, даже если одна из них упадёт. Механизм описан в руководстве по parametrize.
Граница проходит между длиной 7 и 8
True.Не вычисляй expected тем же выражением, которое проверяешь. Если скопировать ошибочное len(password) > 8 в ожидание, функция и тест будут согласованно ошибаться. Здесь ожидаемые значения заданы вручную по условию «не меньше восьми».
Запустить один случай или увидеть все имена
После замены тестового файла пригодятся три команды:
| Задача | Команда |
|---|---|
| Короткий итог всех тестов | python -m pytest -q |
| Имена случаев и их результаты | python -m pytest -v |
| Только строка минимальной длины | python -m pytest "test_passwords.py::test_password_length[minimum]" -q |
В третьей команде :: отделяет имя файла от имени функции, а [minimum] выбирает значение из ids. Кавычки защищают квадратные скобки от обработки оболочкой.
Если тесты не запускаются
Провал проверки и ошибка запуска требуют разных действий. При FAILED pytest уже выполнил тест и обнаружил проблему. При ошибке импорта он мог не добраться до assert.
| Сообщение | Что проверить первым |
|---|---|
No module named pytest | Активировано ли окружение, в котором выполнена установка; повтори python -m pip install pytest в нужном окружении |
No module named passwords | Команда запускается из папки с обоими файлами; файл называется passwords.py |
no tests ran | Тест сохранён в test_passwords.py, а имя функции начинается с test_ |
fixture 'password' not found | Есть ли над функцией декоратор @pytest.mark.parametrize и указан ли в нём параметр password |
AssertionError: assert False is True | Сравни вход, ожидаемый ответ и условие функции; в нашем примере нужна включительная граница >= 8 |
По умолчанию pytest ищет файлы test_*.py и *_test.py. Если назвать файл password_checks.py, обычный запуск без указания пути его не подхватит. И не называй свой файл pytest.py: он может перекрыть установленный пакет при импорте.
Закрепить на другом условии
Нынешняя функция принимает строки и проверяет только длину. Обработка None, правила для цифр и букв, ограничения на состав пароля сюда не входят. Добавляй тесты на них тогда, когда они появятся в условии задачи.
Для следующего упражнения задай диапазон длины: от 8 до 12 включительно. Сначала добавь пример длиной 13 с ожидаемым False. Убедись, что текущая функция его не проходит; затем исправь условие. Случай длиной 12 уже есть в наборе, поэтому тесты проверят обе стороны верхней границы.
Дальше попробуй задачи «Валидаторы на Python»: до решения каждой составь небольшую таблицу входов и ответов, а для локального проекта преврати её в parametrize. Если хочешь расширить сам валидатор, в разборе проверки пароля показано, как собрать несколько нарушений и учесть особенности Unicode.