diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fbbefc1e622..656c1d27da2 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 contains a certain text + with the `match_info` 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 18da7ac23ea..f0d53bb2ace 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1145,6 +1145,12 @@ def raises(expected_exception, *args, **kwargs): ... >>> assert str(exc_info.value) == "value must be <= 10" + Or you can use the keyword argument ``match_info`` to assert that the + exception contains a certain text:: + + >>> with raises(ValueError, match_info='must be 0'): + ... if value != 0: + ... raise ValueError("value must be 0 or None") Or you can specify a callable by passing a to-be-called lambda:: @@ -1194,11 +1200,15 @@ def raises(expected_exception, *args, **kwargs): raise TypeError(msg % type(expected_exception)) message = "DID NOT RAISE {0}".format(expected_exception) + match_info = None if not args: if "message" in kwargs: message = kwargs.pop("message") - return RaisesContext(expected_exception, message) + if "match_info" in kwargs: + match_info = kwargs.pop("match_info") + message += " with text '{0}'".format(match_info) + return RaisesContext(expected_exception, message, match_info) elif isinstance(args[0], str): code, = args assert isinstance(code, str) @@ -1222,9 +1232,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_info): self.expected_exception = expected_exception self.message = message + self.match_info = match_info self.excinfo = None def __enter__(self): @@ -1243,6 +1254,8 @@ def __exit__(self, *tp): exc_type, value, traceback = tp tp = exc_type, exc_type(value), traceback self.excinfo.__init__(tp) + if self.match_info and not self.match_info.lower() in str(self.excinfo).lower(): + pytest.fail(self.message) suppress_exception = issubclass(self.excinfo.type, self.expected_exception) if sys.version_info[0] == 2 and suppress_exception: sys.exc_clear() diff --git a/testing/python/raises.py b/testing/python/raises.py index 08edd5c6a65..8fd1b154fcd 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -118,3 +118,16 @@ def __call__(self): for o in gc.get_objects(): assert type(o) is not T + + def test_raises_match_info(self): + msg = "with base 10" + with pytest.raises(ValueError, match_info=msg): + int('asdf') + + try: + with pytest.raises(ValueError, match_info=msg): + int('asdf', base=16) + except pytest.raises.Exception as e: + assert e.msg == "DID NOT RAISE {0} with text '{1}'".format(repr(ValueError), msg) + else: + assert False, "Expected pytest.raises.Exception"