В тесте подготовка данных выполняется внутри блока pytest.raises. Как сузить область проверки, чтобы ожидаемое исключение относилось только к целевому вызову?
Разместите внутри блока pytest.raises только тот вызов, от которого ожидается исключение. Подготовку данных, проверки предусловий и прочие операции выполняйте до входа в блок.
Так тест не засчитает исключение, возникшее на этапе подготовки, как ожидаемый результат целевой операции.
Механизм pytest.raises появился как декларативная замена ручным конструкциям с try/except, флагами и явной проверкой факта исключения. Он делает ожидаемое исключение частью условия теста и автоматически завершает тест ошибкой, если исключение не возникло или оказалось другого типа.
Контекстный менеджер особенно полезен для локализации проверки: границы блока явно показывают, какой именно участок кода должен завершиться исключением.
Если поместить в блок pytest.raises несколько операций, исключение от любой из них может быть принято за ожидаемое. В результате тест формально проходит, хотя целевая функция вообще не была вызвана или не выполнила проверяемую валидацию.
Такая ошибка опасна тем, что тест проверяет наличие исключения, но не проверяет его источник. При изменении подготовки данных или порядка действий тест может продолжить проходить по неправильной причине.
Блок pytest.raises действует как контекстный менеджер. При выходе из него pytest проверяет, возникло ли исключение указанного типа; ожидаемое исключение поглощается и считается успешным результатом теста.
Поэтому границы блока должны быть минимальными:
В примере подготовка значения выполняется до проверки. Если ошибка возникнет в build_invalid_value, она не будет ошибочно засчитана как ожидаемая ошибка validate.
Указывайте наиболее конкретный ожидаемый тип исключения. Проверка базового типа может принять подкласс и тем самым скрыть более точную ошибку в реализации.
При необходимости можно дополнительно проверять сообщение через параметр match, но это следует делать только для значимой части сообщения: полное сравнение делает тест хрупким при безвредных изменениях формулировки.
В тесте API-клиента сначала формировался некорректный URL, а затем выполнялся вызов клиента внутри одного блока pytest.raises. Из-за ошибки разбора URL тест проходил, хотя проверка ответа на недопустимый параметр не работала.
Рассматривались два варианта. Можно было оставить широкий блок и проверять сообщение исключения, но это не гарантировало бы, что исключение выброшено именно клиентом. Другой вариант — разделить подготовку и целевой вызов; он сделал источник ошибки однозначным и упростил диагностику.
Выбран был второй вариант. После этого изменение парсинга URL сразу стало ломать тест подготовки, а отсутствие валидации параметра — тест целевого вызова, поэтому дефекты перестали маскировать друг друга.
Тест завершится ошибкой: pytest.raises ожидает исключение и считает отсутствие исключения нарушением условия теста. Это отличает его от обычного try/except, где разработчик может случайно забыть установить или проверить признак успешного сценария.
Да. Проверка типа выполняется с учетом наследования, поэтому исключение-подкласс обычно считается подходящим. Если важен строго конкретный класс без подклассов, одной стандартной проверки pytest.raises недостаточно: нужно отдельно получить исключение и сравнить его тип явно.
match проверяет сообщение исключения по регулярному выражению, но сам по себе не определяет корректность типа. Сообщение может совпасть у разных исключений, поэтому надежная проверка обычно сочетает конкретный тип исключения с проверкой только существенного фрагмента сообщения.