diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fbbefc1e6..816d3eb5b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,9 @@ New Features * ``pytest.warns`` now checks for subclass relationship rather than class equality. Thanks `@lesteve`_ for the PR (`#2166`_) +* ``pytest.raises`` now asserts that the error message matches a text or regex + with the ``match`` keyword argument. Thanks `@Kriechi`_ for the PR. + Changes ------- @@ -56,6 +59,7 @@ Changes .. _@fogo: https://github.com/fogo .. _@mandeep: https://github.com/mandeep .. _@unsignedint: https://github.com/unsignedint +.. _@Kriechi: https://github.com/Kriechi .. _#1512: https://github.com/pytest-dev/pytest/issues/1512 .. _#1874: https://github.com/pytest-dev/pytest/pull/1874 diff --git a/_pytest/python.py b/_pytest/python.py index 18da7ac23..ff4edff5b 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1134,7 +1134,7 @@ def raises(expected_exception, *args, **kwargs): >>> with raises(ValueError) as exc_info: ... if value > 10: ... raise ValueError("value must be <= 10") - ... assert str(exc_info.value) == "value must be <= 10" # this will not execute + ... assert exc_info.type == ValueError # this will not execute Instead, the following approach must be taken (note the difference in scope):: @@ -1143,7 +1143,16 @@ def raises(expected_exception, *args, **kwargs): ... if value > 10: ... raise ValueError("value must be <= 10") ... - >>> assert str(exc_info.value) == "value must be <= 10" + >>> assert exc_info.type == ValueError + + Or you can use the keyword argument ``match`` to assert that the + exception matches a text or regex:: + + >>> with raises(ValueError, match='must be 0 or None'): + ... raise ValueError("value must be 0 or None") + + >>> with raises(ValueError, match=r'must be \d+$'): + ... raise ValueError("value must be 42") Or you can specify a callable by passing a to-be-called lambda:: @@ -1194,11 +1203,15 @@ def raises(expected_exception, *args, **kwargs): raise TypeError(msg % type(expected_exception)) message = "DID NOT RAISE {0}".format(expected_exception) + match_expr = None if not args: if "message" in kwargs: message = kwargs.pop("message") - return RaisesContext(expected_exception, message) + if "match" in kwargs: + match_expr = kwargs.pop("match") + message += " matching '{0}'".format(match_expr) + return RaisesContext(expected_exception, message, match_expr) elif isinstance(args[0], str): code, = args assert isinstance(code, str) @@ -1222,9 +1235,10 @@ def raises(expected_exception, *args, **kwargs): pytest.fail(message) class RaisesContext(object): - def __init__(self, expected_exception, message): + def __init__(self, expected_exception, message, match_expr): self.expected_exception = expected_exception self.message = message + self.match_expr = match_expr self.excinfo = None def __enter__(self): @@ -1246,6 +1260,8 @@ class RaisesContext(object): suppress_exception = issubclass(self.excinfo.type, self.expected_exception) if sys.version_info[0] == 2 and suppress_exception: sys.exc_clear() + if self.match_expr: + self.excinfo.match(self.match_expr) return suppress_exception diff --git a/testing/python/raises.py b/testing/python/raises.py index 08edd5c6a..8170ec793 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -118,3 +118,18 @@ class TestRaises: for o in gc.get_objects(): assert type(o) is not T + + def test_raises_match(self): + msg = r"with base \d+" + with pytest.raises(ValueError, match=msg): + int('asdf') + + msg = "with base 10" + with pytest.raises(ValueError, match=msg): + int('asdf') + + msg = "with base 16" + expr = r"Pattern '{0}' not found in 'invalid literal for int\(\) with base 10: 'asdf''".format(msg) + with pytest.raises(AssertionError, match=expr): + with pytest.raises(ValueError, match=msg): + int('asdf', base=10) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 40d11cd04..231ef028e 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -112,10 +112,9 @@ class TestDeprecatedCall(object): pytest.deprecated_call(self.dep_explicit, 0) def test_deprecated_call_as_context_manager_no_warning(self): - with pytest.raises(pytest.fail.Exception) as ex: + with pytest.raises(pytest.fail.Exception, matches='^DID NOT WARN'): with pytest.deprecated_call(): self.dep(1) - assert str(ex.value).startswith("DID NOT WARN") def test_deprecated_call_as_context_manager(self): with pytest.deprecated_call():