Pytest для начинающих: первый тест и поиск ошибки на границе

Python Автор: Среда и версия: CPython 3.14.7 + pytest 9.1.1; команды проверены в Linux
содержание

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.

Python / 01

Один вход показывает ошибку и проверяет исправление

Проверка строки длиной восемь до и после исправленияШаг 1: строка abcd1234 содержит восемь символов, ожидаемый результат True. Шаг 2: условие len(password) больше 8 возвращает False, тест падает. Шаг 3: условие len(password) больше или равно 8 возвращает True, тот же тест проходит.01 / записать ожидание"abcd1234" · длина 8ожидаем True02 / запустить тестlen(password) > 8False → FAILED03 / исправить и повторитьlen(password) >= 8True → PASSED
Вход и ожидание остаются прежними. Заменяется знак сравнения в функции, после чего тот же тест проходит.

Исправить условие и сохранить проверку

Замени содержимое passwords.py:

def is_long_enough(password: str) -> bool:
    return len(password) >= 8

Снова выполни python -m pytest -q. Теперь итог — 1 passed.

Тест оставь: если при будущей правке в функцию вернётся строгое >, он снова упадёт. Это регрессионная проверка, то есть проверка против возврата уже найденной ошибки.

Один зелёный тест ещё не означает, что функция верна на всех входах. Например, реализация return True тоже прошла бы эту проверку. Значит, нужны случаи, для которых ответ должен быть False.

Проверить несколько случаев через parametrize

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

СлучайСтрокаДлинаОжидаемЧто проверяем
Пустая строка""0FalseОтсутствие символов
Ниже границы"abcd123"7FalseСлишком короткая строка не проходит
На границе"abcd1234"8TrueМинимальная длина включена
Выше границы"abcd12345"9TrueСледующее значение тоже проходит
Обычная длинная"a1b2c3d4e5f6"12TrueРабочий случай вдали от границы

@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.

Python / 02

Граница проходит между длиной 7 и 8

Пять входов для условия длина больше или равна восьмиШаг 1: сравнить длину с порогом восемь. Длины пустой строки, строки ниже границы, на границе, выше границы и обычной строки равны 0, 7, 8, 9 и 12. Полосы начинаются от нуля и имеют общий масштаб. Шаг 2: проверить результат. Для длин 0 и 7 ожидается False, для 8, 9 и 12 ожидается True.01 / сравнить с порогом02 / ответминимум 8Пустая0FalseНиже границы7FalseНа границе8TrueВыше границы9TrueОбычная12True04812длина
Длины взяты из таблицы тестов; одна единица шкалы — один ASCII-символ в этих строках. Проверки на 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.

Источники