Проверьте поведение проверки сообщения исключения: пройдёт ли этот тест, хотя шаблон не совпадает со всей с...

Проверьте поведение проверки сообщения исключения: пройдёт ли этот тест, хотя шаблон не совпадает со всей строкой?

import pytest


def parse_port(value):
    if not value.isdigit():
        raise ValueError(f"invalid port: {value}")


def test_parse_port():
    with pytest.raises(ValueError, match="port: 80"):
        parse_port("8080x")
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Тест пройдёт. Параметр match в pytest.raises интерпретируется как регулярное выражение и проверяется поиском совпадения внутри текста исключения, а не сравнением всей строки.

Сообщение исключения — "invalid port: 8080x", а шаблон "port: 80" находит в нём подходящий фрагмент. Для проверки всей строки нужны якоря регулярного выражения ^ и $ либо явно экранированный ожидаемый текст.

Исторический контекст

Проверка исключений появилась как более точная альтернатива ручной конструкции try/except, где тест должен был самостоятельно определить, было ли исключение выброшено и правильно ли сформировано его сообщение.

В pytest контекстный менеджер pytest.raises отделяет проверку типа исключения от проверки его текста. Поддержка регулярных выражений позволяет проверять устойчивую часть сообщения, не привязывая тест к динамическим данным.

Постановка проблемы

Если разработчик ожидает полное равенство сообщения, но передаёт в match обычную строку без якорей, тест может пропустить неправильный текст. Это создаёт ложноположительный результат: исключение имеет нужный тип, но сообщение содержит неожиданные дополнительные данные или даже другой контекст.

Обратная ситуация тоже опасна. Если динамическое значение содержит символы вроде ., +, [ или ?, строка в match будет обработана как регулярное выражение, а не как буквальный текст.

Подробное решение

pytest.raises проверяет три основных условия:

  1. внутри блока было выброшено исключение;
  2. его тип соответствует указанному типу или одному из типов в кортеже;
  3. при заданном match текст исключения совпадает с регулярным выражением.

Проверка match выполняется поиском по строковому представлению исключения, аналогично re.search. Поэтому шаблон может соответствовать только части сообщения:

import re import pytest def test_exact_message(): with pytest.raises( ValueError, match=r"^invalid port: 8080x$", ): raise ValueError("invalid port: 8080x") def test_literal_message(): expected = "invalid port: 8080x" with pytest.raises(ValueError, match=re.escape(expected)): raise ValueError(expected)

Якоря ^ и $ требуют совпадения с началом и концом строки. re.escape превращает пользовательский или динамический текст в буквальный шаблон и предотвращает интерпретацию специальных символов регулярного выражения.

Важно, что match проверяет текст исключения, а не его атрибуты. Для исключений с дополнительными данными, например кодом ошибки, такие поля следует проверять отдельно через объект, возвращённый из контекстного менеджера:

with pytest.raises(MyError) as exc_info: service.run() assert exc_info.value.code == "PORT_INVALID"

Слишком точная проверка всего сообщения повышает чувствительность теста к косметическим изменениям текста. Проверка только короткого фрагмента, напротив, может скрыть ошибку. Выбор зависит от контракта: если сообщение является публичным API, проверяйте его точнее; если важен только диагностический фрагмент, используйте частичное совпадение.

Ситуация из практики

В API-сервисе ошибка валидации содержала имя поля и значение, например "invalid port: 8080x". Команда сначала использовала match="invalid port", поэтому тест продолжал проходить даже после изменения формата сообщения на "invalid port; request rejected".

Рассматривались два варианта. Полное сравнение строки было строгим, но делало тест хрупким при добавлении диагностических деталей. Частичное регулярное выражение было стабильнее, однако не гарантировало корректность всего сообщения.

Выбрали проверку стабильного контракта через match=r"^invalid port: 8080x$" для отдельного теста формата и отдельные тесты атрибутов исключения для машинно обрабатываемых данных. Это разделило проверку пользовательского текста и проверку структурированных полей.

Что кандидаты часто упускают

  1. Равен ли match="text" полному сравнению сообщения?

Нет. Это регулярное выражение, применяемое поиском внутри текста исключения. Полное совпадение нужно выразить якорями, например match=r"^text$", учитывая, что перенос строки и особенности регулярного выражения тоже влияют на результат.

  1. Как безопасно проверить сообщение, полученное из переменной?

Нельзя бездумно передавать переменную в match, если она может содержать специальные символы regex. Следует использовать re.escape(expected); если требуется полное совпадение, добавить якоря: match=rf"^{re.escape(expected)}$".

  1. Что произойдёт, если внутри pytest.raises исключение не возникнет?

Тест завершится ошибкой, потому что контекстный менеджер ожидает исключение. Если будет выброшен другой тип, тест также завершится ошибкой; match в таком случае не заменяет проверку типа и не делает любой тип исключения допустимым.