Skip to content

Commit

Permalink
Merge remote-tracking branch 'perfa/feature_raises_matcher'
Browse files Browse the repository at this point in the history
Resolves #30
  • Loading branch information
offbyone committed Jul 7, 2013
2 parents f03de2f + ef7d987 commit 9bd9cde
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 0 deletions.
6 changes: 6 additions & 0 deletions doc/object_matchers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ same_instance
^^^^^^^^^^^^^

.. automodule:: hamcrest.core.core.issame

calling, raises
^^^^^^^^^^^^^^^

.. automodule:: hamcrest.core.core.raises

20 changes: 20 additions & 0 deletions doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ This is equivalent to the :py:meth:`~unittest.TestCase.assert_` method of
offers greater flexibility in test writing.


Asserting Exceptions
--------------------
There's a utility function and matcher available to help you test that your
code has the expected behavior in situations where it should raise an exception.
The :py:func:`~hamcrest.core.core.raises.calling` function wraps a callable,
and then allows you to set arguments to be used in a call to the wrapped callable. This,
together with the :py:func:`~hamcrest.core.core.raises.raises` matcher lets you
assert that calling a method with certain arguments causes an exception to be thrown. It is also possible to provide a regular expression pattern to the :py:func:`~hamcrest.core.core.raises.raises` matcher allowing you assure that the right issue was found::

assert_that(calling(parse, bad_data), raises(ValueError))
assert_that(calling(translate).with_(curse_words), raises(LanguageError, "\w+very naughty"))
assert_that(broken_function, raises(Exception))
# This will fail and complain that 23 is not callable
# assert_that(23, raises(IOError))



Predefined matchers
-------------------

Expand All @@ -86,6 +103,9 @@ PyHamcrest comes with a library of useful matchers:
:py:func:`~hamcrest.core.core.isnone.not_none` - match ``None``, or not
``None``
* :py:func:`~hamcrest.core.core.issame.same_instance` - match same object
* :py:func:`~hamcrest.core.core.raises.calling`,
:py:func:`~hamcrest.core.core.raises.raises` - wrap a method call and assert
that it raises an exception

* Number

Expand Down
19 changes: 19 additions & 0 deletions hamcrest/core/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
__author__ = "Per Fagrell"
__copyright__ = "Copyright 2013 hamcrest.org"
__license__ = "BSD, see License.txt"

__all__ = ['is_callable']

import sys

# callable was not part of py3k until 3.2, so we create this
# generic is_callable to use callable if possible, otherwise
# we use generic homebrew.
if sys.version_info[0] == 3 and sys.version_info[1] < 2:
def is_callable(function):
"""Return whether the object is callable (i.e., some kind of function)."""
if function is None:
return False
return any("__call__" in klass.__dict__ for klass in type(function).__mro__)
else:
is_callable = callable
1 change: 1 addition & 0 deletions hamcrest/core/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from isnone import none, not_none
from isnot import is_not
from issame import same_instance
from raises import calling, raises

__author__ = "Jon Reid"
__copyright__ = "Copyright 2011 hamcrest.org"
Expand Down
108 changes: 108 additions & 0 deletions hamcrest/core/core/raises.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from weakref import ref
import re
import sys
from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.helpers.wrap_matcher import wrap_matcher
from hamcrest.core.compat import is_callable

__author__ = "Per Fagrell"
__copyright__ = "Copyright 2013 hamcrest.org"
__license__ = "BSD, see License.txt"


class Raises(BaseMatcher):
def __init__(self, expected, pattern=None):
self.pattern = pattern
self.expected = expected
self.actual = None
self.function = None

def _matches(self, function):
if not is_callable(function):
return False

self.function = ref(function)
return self._call_function(function)

def _call_function(self, function):
self.actual = None
try:
function()
except Exception:
self.actual = sys.exc_info()[1]

if isinstance(self.actual, self.expected):
if self.pattern is not None:
return re.search(self.pattern, str(self.actual)) is not None
return True
return False

def describe_to(self, description):
description.append_text('Expected a callable raising %s' % self.expected)

def describe_mismatch(self, item, description):
if not is_callable(item):
description.append_text('%s is not callable' % item)
return

function = None if self.function is None else self.function()
if function is None or function is not item:
self.function = ref(item)
if not self._call_function(item):
return

if self.actual is None:
description.append_text('No exception raised.')
elif isinstance(self.actual, self.expected) and self.pattern is not None:
description.append_text('Correct assertion type raised, but the expected pattern ("%s") not found.' % self.pattern)
description.append_text('\n message was: "%s"' % str(self.actual))
else:
description.append_text('%s was raised instead' % type(self.actual))


def raises(exception, pattern=None):
"""Matches if the called function raised the expected exception.
:param exception: The class of the expected exception
:param pattern: Optional regular expression to match exception message.
Expects the actual to be wrapped by using :py:func:`~hamcrest.core.core.raises.calling`,
or a callable taking no arguments.
Optional argument pattern should be a string containing a regular expression. If provided,
the string representation of the actual exception - e.g. `str(actual)` - must match pattern.
Examples::
assert_that(calling(int).with_args('q'), raises(TypeError))
assert_that(calling(parse, broken_input), raises(ValueError))
"""
return Raises(exception, pattern)


class DeferredCallable(object):
def __init__(self, func):
self.func = func
self.args = tuple()
self.kwargs = {}

def __call__(self):
self.func(*self.args, **self.kwargs)

def with_args(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
return self


def calling(func):
"""Wrapper for function call that delays the actual execution so that
:py:func:`~hamcrest.core.core.raises.raises` matcher can catch any thrown exception.
:param func: The function or method to be called
The arguments can be provided with a call to the `with_args` function on the returned
object::
calling(my_method).with_args(arguments, and_='keywords')
"""
return DeferredCallable(func)
102 changes: 102 additions & 0 deletions hamcrest_unit_test/core/raises_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
if __name__ == '__main__':
import sys
sys.path.insert(0, '..')
sys.path.insert(0, '../..')

from hamcrest.core.core.raises import *

from hamcrest.core.core.isequal import equal_to
from hamcrest_unit_test.matcher_test import MatcherTest
import unittest

__author__ = "Per Fagrell"
__copyright__ = "Copyright 2013 hamcrest.org"
__license__ = "BSD, see License.txt"


def no_exception(*args, **kwargs):
return


def raise_exception(*args, **kwargs):
raise AssertionError(str(args) + str(kwargs))


class RaisesTest(MatcherTest):
def testMatchesIfFunctionRaisesTheExactExceptionExpected(self):
self.assert_matches('Right exception',
raises(AssertionError),
calling(raise_exception))

def testDoesNotMatchTypeErrorIfActualIsNotCallable(self):
self.assert_does_not_match('Not callable',
raises(TypeError),
23)

def testMatchesIfFunctionRaisesASubclassOfTheExpectedException(self):
self.assert_matches('Subclassed Exception',
raises(Exception),
calling(raise_exception))

def testDoesNotMatchIfFunctionDoesNotRaiseException(self):
self.assert_does_not_match('No exception',
raises(ValueError),
calling(no_exception))

def testDoesNotMatchExceptionIfRegularExpressionDoesNotMatch(self):
self.assert_does_not_match('Bad regex',
raises(AssertionError, "Phrase not found"),
calling(raise_exception))

def testMatchesRegularExpressionToStringifiedException(self):
self.assert_matches('Regex',
raises(AssertionError, "(3, 1, 4)"),
calling(raise_exception).with_args(3,1,4))

self.assert_matches('Regex',
raises(AssertionError, "([\d, ]+)"),
calling(raise_exception).with_args(3,1,4))

def testDescribeMismatchWillCallItemIfNotTheOriginalMatch(self):
function = Callable()
matcher = raises(AssertionError)
matcher.describe_mismatch(function, object())
self.assertTrue(function.called)

class CallingTest(unittest.TestCase):
def testCallingDoesNotImmediatelyExecuteFunction(self):
try:
calling(raise_exception)
except AssertionError:
self.fail()
else:
pass

def testCallingObjectCallsProvidedFunction(self):
method = Callable()
calling(method)()
self.assertTrue(method.called)

def testCallingWithFunctionReturnsObject(self):
method = Callable()
callable = calling(method)
returned = callable.with_args(3, 1, 4, keyword1="arg1")

self.assertEqual(returned, callable)

def testCallingWithFunctionSetsArgumentList(self):
method = Callable()
calling(method).with_args(3, 1, 4, keyword1="arg1")()

self.assertEqual(method.args, (3, 1, 4))
self.assertEqual(method.kwargs, {"keyword1": "arg1"})


class Callable(object):
def __init__(self):
self.called = False

def __call__(self, *args, **kwargs):
self.called = True
self.args = args
self.kwargs = kwargs

0 comments on commit 9bd9cde

Please sign in to comment.