← Back to team overview

testtools-dev team mailing list archive

[Merge] lp:~lifeless/testtools/matchers into lp:testtools

 

Robert Collins has proposed merging lp:~lifeless/testtools/matchers into lp:testtools.

Requested reviews:
  testtools developers (testtools-dev)


This adds polished matchers to replace assertRaises, which I first prototyped in testrepository.

The final commit on the branch uses the new Raises and MatchesException (ugh, perhaps a shorter name would be good?) throughout testtools own tests (except where we are testing assertRaises itself).

I included this (largely mechanical) change in the branch so that the new matchers can be asessed properly. I considered a sugar to replace lambda: but actually found that the lambda: spelling grew on me pretty quickly: its much clearer about *what is being called* than the magic call-forward approach that assertRaises uses.
-- 
https://code.launchpad.net/~lifeless/testtools/matchers/+merge/40606
Your team testtools developers is requested to review the proposed merge of lp:~lifeless/testtools/matchers into lp:testtools.
=== modified file 'MANUAL'
--- MANUAL	2010-10-26 18:59:12 +0000
+++ MANUAL	2010-11-11 09:52:45 +0000
@@ -150,6 +150,13 @@
         self.assertEqual('bob', error.username)
         self.assertEqual('User bob cannot frobnicate', str(error))
 
+Note that this is incompatible with the assertRaises in unittest2/Python2.7.
+While we have no immediate plans to change to be compatible consider using the
+new assertThat facility instead::
+
+        self.assertThat(thing.frobnicate,
+            Raises(MatchesException(UnauthorisedError('bob')))
+
 
 TestCase.assertThat
 ~~~~~~~~~~~~~~~~~~~

=== modified file 'Makefile'
--- Makefile	2010-07-04 07:19:58 +0000
+++ Makefile	2010-11-11 09:52:45 +0000
@@ -16,15 +16,20 @@
 	rm -f TAGS tags
 	find testtools -name "*.pyc" -exec rm '{}' \;
 
-release:
+prerelease:
 	# An existing MANIFEST breaks distutils sometimes. Avoid that.
 	-rm MANIFEST
+
+release:
 	./setup.py sdist upload --sign
 
+snapshot: prerelease
+	./setup.py sdist
+
 apidocs:
 	pydoctor --make-html --add-package testtools \
 		--docformat=restructuredtext --project-name=testtools \
 		--project-url=https://launchpad.net/testtools
 
 
-.PHONY: check clean release apidocs
+.PHONY: check clean prerelease release apidocs

=== modified file 'NEWS'
--- NEWS	2010-10-31 16:42:36 +0000
+++ NEWS	2010-11-11 09:52:45 +0000
@@ -7,6 +7,9 @@
 Improvements
 ------------
 
+* addUnexpectedSuccess is translated to addFailure for test results that don't
+  know about addUnexpectedSuccess.  (Jonathan Lange, #654474)
+
 * Experimental support for running tests that return Deferreds.
   (Jonathan Lange, Martin [gz])
 
@@ -21,8 +24,12 @@
 
 * Malformed SyntaxErrors no longer blow up the test suite.  (Martin [gz])
 
-* addUnexpectedSuccess is translated to addFailure for test results that don't
-  know about addUnexpectedSuccess.  (Jonathan Lange, #654474)
+* ``MatchesException`` added to the ``testtools.matchers`` module - matches
+  an exception class and parameters. (Robert Collins)
+
+* ``Raises`` added to the ``testtools.matchers`` module - matches if the 
+  supplied callable raises, and delegates to an optional matcher for validation
+  of the exception. (Robert Collins)
 
 * ``testools.TestCase.useFixture`` has been added to glue with fixtures nicely.
   (Robert Collins)

=== modified file 'testtools/matchers.py'
--- testtools/matchers.py	2010-10-31 16:25:47 +0000
+++ testtools/matchers.py	2010-11-11 09:52:45 +0000
@@ -20,14 +20,17 @@
     'LessThan',
     'MatchesAll',
     'MatchesAny',
+    'MatchesException',
     'NotEquals',
     'Not',
+    'Raises',
     'StartsWith',
     ]
 
 import doctest
 import operator
 from pprint import pformat
+import sys
 
 
 class Matcher(object):
@@ -103,6 +106,10 @@
         """
         return getattr(self, '_details', {})
 
+    def __repr__(self):
+        return  "<testtools.matchers.Mismatch object at %x attributes=%r>" % (
+            id(self), self.__dict__)
+
 
 class DocTestMatches(object):
     """See if a string matches a doctest example."""
@@ -323,6 +330,40 @@
         return "%r matches %s" % (self.other, self.matcher)
 
 
+class MatchesException(Matcher):
+    """Match an exc_info tuple against an exception instance or type."""
+
+    def __init__(self, exception):
+        """Create a MatchesException that will match exc_info's for exception.
+        
+        :param exception: Either an exception instance or type.
+            If an instance is given, the type and arguments of the exception
+            are checked. If a type is given only the type of the exception is
+            checked.
+        """
+        Matcher.__init__(self)
+        self.expected = exception
+
+    def _expected_type(self):
+        if type(self.expected) is type:
+            return self.expected
+        return type(self.expected)
+
+    def match(self, other):
+        if type(other) != tuple:
+            return Mismatch('%r is not an exc_info tuple' % other)
+        if not issubclass(other[0], self._expected_type()):
+            return Mismatch('%r is not a %r' % (
+                other[0], self._expected_type()))
+        if (type(self.expected) is not type and
+            other[1].args != self.expected.args):
+            return Mismatch('%r has different arguments to %r.' % (
+                other[1], self.expected))
+
+    def __str__(self):
+        return "MatchesException(%r)" % self.expected
+
+
 class StartsWith(Matcher):
     """Checks whether one string starts with another."""
 
@@ -398,3 +439,46 @@
 
     def describe(self):
         return '%s: %s' % (self.mismatch.describe(), self.annotation)
+
+
+class Raises(Matcher):
+    """Match if the matchee raises an exception when called.
+    
+    Exceptions which are not subclasses of Exception propogate out of the
+    Raises.match call unless they are explicitly matched.
+    """
+
+    def __init__(self, exception_matcher=None):
+        """Create a Raises matcher. 
+        
+        :param exception_matcher: Optional validator for the exception raised
+            by matchee. If supplied the exc_info tuple for the exception raised
+            is passed into that matcher. If no exception_matcher is supplied
+            then the simple fact of raising an exception is considered enough
+            to match on.
+        """
+        self.exception_matcher = exception_matcher
+
+    def match(self, matchee):
+        try:
+            result = matchee()
+            return Mismatch('%r returned %r' % (matchee, result))
+        # Catch all exceptions: Raises() should be able to match a
+        # KeyboardInterrupt or SystemExit.
+        except:
+            exc_info = sys.exc_info()
+            if self.exception_matcher:
+                mismatch = self.exception_matcher.match(sys.exc_info())
+                if not mismatch:
+                    return
+            else:
+                mismatch = None
+            # The exception did not match, or no explicit matching logic was
+            # performed. If the exception is a non-user exception (that is, not
+            # a subclass of Exception) then propogate it.
+            if not issubclass(exc_info[0], Exception):
+                raise exc_info[0], exc_info[1], exc_info[2]
+            return mismatch
+
+    def __str__(self):
+        return 'Raises()'

=== modified file 'testtools/tests/test_compat.py'
--- testtools/tests/test_compat.py	2010-06-23 00:32:51 +0000
+++ testtools/tests/test_compat.py	2010-11-11 09:52:45 +0000
@@ -17,6 +17,10 @@
     _u,
     unicode_output_stream,
     )
+from testtools.matchers import (
+    MatchesException,
+    Raises,
+    )
 
 
 class TestDetectEncoding(testtools.TestCase):
@@ -241,7 +245,8 @@
         soutwrapper = unicode_output_stream(sout)
         if newio:
             self.expectFailure("Python 3 StringIO expects text not bytes",
-                self.assertRaises, TypeError, soutwrapper.write, self.uni)
+                self.assertThat, lambda: soutwrapper.write(self.uni),
+                Raises(MatchesException(TypeError)))
         soutwrapper.write(self.uni)
         self.assertEqual("pa???n", sout.getvalue())
 

=== modified file 'testtools/tests/test_content.py'
--- testtools/tests/test_content.py	2010-10-28 22:06:41 +0000
+++ testtools/tests/test_content.py	2010-11-11 09:52:45 +0000
@@ -5,16 +5,21 @@
 from testtools.compat import _u
 from testtools.content import Content, TracebackContent, text_content
 from testtools.content_type import ContentType, UTF8_TEXT
+from testtools.matchers import MatchesException, Raises
 from testtools.tests.helpers import an_exc_info
 
 
+raises_value = Raises(MatchesException(ValueError))
+
+
 class TestContent(TestCase):
 
     def test___init___None_errors(self):
-        self.assertRaises(ValueError, Content, None, None)
-        self.assertRaises(ValueError, Content, None, lambda: ["traceback"])
-        self.assertRaises(ValueError, Content,
-            ContentType("text", "traceback"), None)
+        self.assertThat(lambda:Content(None, None), raises_value)
+        self.assertThat(lambda:Content(None, lambda: ["traceback"]),
+            raises_value)
+        self.assertThat(lambda:Content(ContentType("text", "traceback"), None),
+            raises_value)
 
     def test___init___sets_ivars(self):
         content_type = ContentType("foo", "bar")
@@ -37,7 +42,7 @@
     def test_iter_text_not_text_errors(self):
         content_type = ContentType("foo", "bar")
         content = Content(content_type, lambda: ["bytes"])
-        self.assertRaises(ValueError, content.iter_text)
+        self.assertThat(content.iter_text, raises_value)
 
     def test_iter_text_decodes(self):
         content_type = ContentType("text", "strange", {"charset": "utf8"})
@@ -56,7 +61,7 @@
 class TestTracebackContent(TestCase):
 
     def test___init___None_errors(self):
-        self.assertRaises(ValueError, TracebackContent, None, None)
+        self.assertThat(lambda:TracebackContent(None, None), raises_value) 
 
     def test___init___sets_ivars(self):
         content = TracebackContent(an_exc_info, self)

=== modified file 'testtools/tests/test_content_type.py'
--- testtools/tests/test_content_type.py	2010-08-05 10:04:56 +0000
+++ testtools/tests/test_content_type.py	2010-11-11 09:52:45 +0000
@@ -1,16 +1,17 @@
 # Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
 
 from testtools import TestCase
-from testtools.matchers import Equals
+from testtools.matchers import Equals, MatchesException, Raises
 from testtools.content_type import ContentType, UTF8_TEXT
 
 
 class TestContentType(TestCase):
 
     def test___init___None_errors(self):
-        self.assertRaises(ValueError, ContentType, None, None)
-        self.assertRaises(ValueError, ContentType, None, "traceback")
-        self.assertRaises(ValueError, ContentType, "text", None)
+        raises_value = Raises(MatchesException(ValueError))
+        self.assertThat(lambda:ContentType(None, None), raises_value)
+        self.assertThat(lambda:ContentType(None, "traceback"), raises_value)
+        self.assertThat(lambda:ContentType("text", None), raises_value)
 
     def test___init___sets_ivars(self):
         content_type = ContentType("foo", "bar")

=== modified file 'testtools/tests/test_deferredruntest.py'
--- testtools/tests/test_deferredruntest.py	2010-10-31 16:33:44 +0000
+++ testtools/tests/test_deferredruntest.py	2010-11-11 09:52:45 +0000
@@ -22,6 +22,8 @@
 from testtools.matchers import (
     Equals,
     KeysEqual,
+    MatchesException,
+    Raises,
     )
 from testtools.runtest import RunTest
 
@@ -396,7 +398,8 @@
         runner = self.make_runner(test, timeout * 5)
         result = self.make_result()
         reactor.callLater(timeout, os.kill, os.getpid(), SIGINT)
-        self.assertRaises(KeyboardInterrupt, runner.run, result)
+        self.assertThat(lambda:runner.run(result),
+            Raises(MatchesException(KeyboardInterrupt)))
 
     @skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only")
     def test_fast_keyboard_interrupt_stops_test_run(self):
@@ -414,7 +417,8 @@
         runner = self.make_runner(test, timeout * 5)
         result = self.make_result()
         reactor.callWhenRunning(os.kill, os.getpid(), SIGINT)
-        self.assertRaises(KeyboardInterrupt, runner.run, result)
+        self.assertThat(lambda:runner.run(result),
+            Raises(MatchesException(KeyboardInterrupt)))
 
     def test_timeout_causes_test_error(self):
         # If a test times out, it reports itself as having failed with a

=== modified file 'testtools/tests/test_matchers.py'
--- testtools/tests/test_matchers.py	2010-10-31 16:25:47 +0000
+++ testtools/tests/test_matchers.py	2010-11-11 09:52:45 +0000
@@ -3,6 +3,7 @@
 """Tests for matchers."""
 
 import doctest
+import sys
 
 from testtools import (
     Matcher, # check that Matcher is exposed at the top level for docs.
@@ -18,9 +19,11 @@
     LessThan,
     MatchesAny,
     MatchesAll,
+    MatchesException,
     Mismatch,
     Not,
     NotEquals,
+    Raises,
     StartsWith,
     )
 
@@ -37,7 +40,8 @@
 
     def test_constructor_no_arguments(self):
         mismatch = Mismatch()
-        self.assertRaises(NotImplementedError, mismatch.describe)
+        self.assertThat(mismatch.describe,
+            Raises(MatchesException(NotImplementedError)))
         self.assertEqual({}, mismatch.get_details())
 
 
@@ -155,6 +159,58 @@
     describe_examples = [('4 is >= 4', 4, LessThan(4))]
 
 
+def make_error(type, *args, **kwargs):
+    try:
+        raise type(*args, **kwargs)
+    except type:
+        return sys.exc_info()
+
+
+class TestMatchesExceptionInstanceInterface(TestCase, TestMatchersInterface):
+
+    matches_matcher = MatchesException(ValueError("foo"))
+    error_foo = make_error(ValueError, 'foo')
+    error_bar = make_error(ValueError, 'bar')
+    error_base_foo = make_error(Exception, 'foo')
+    matches_matches = [error_foo]
+    matches_mismatches = [error_bar, error_base_foo]
+
+    str_examples = [
+        ("MatchesException(Exception('foo',))",
+         MatchesException(Exception('foo')))
+        ]
+    describe_examples = [
+        ("<type 'exceptions.Exception'> is not a "
+         "<type 'exceptions.ValueError'>",
+         error_base_foo,
+         MatchesException(ValueError("foo"))),
+        ("ValueError('bar',) has different arguments to ValueError('foo',).",
+         error_bar,
+         MatchesException(ValueError("foo"))),
+        ]
+
+
+class TestMatchesExceptionTypeInterface(TestCase, TestMatchersInterface):
+
+    matches_matcher = MatchesException(ValueError)
+    error_foo = make_error(ValueError, 'foo')
+    error_sub = make_error(UnicodeError, 'bar')
+    error_base_foo = make_error(Exception, 'foo')
+    matches_matches = [error_foo, error_sub]
+    matches_mismatches = [error_base_foo]
+
+    str_examples = [
+        ("MatchesException(<type 'exceptions.Exception'>)",
+         MatchesException(Exception))
+        ]
+    describe_examples = [
+        ("<type 'exceptions.Exception'> is not a "
+         "<type 'exceptions.ValueError'>",
+         error_base_foo,
+         MatchesException(ValueError)),
+        ]
+
+
 class TestNotInterface(TestCase, TestMatchersInterface):
 
     matches_matcher = Not(Equals(1))
@@ -249,6 +305,67 @@
     describe_examples = [("1 != 2: foo", 2, Annotate('foo', Equals(1)))]
 
 
+class TestRaisesInterface(TestCase, TestMatchersInterface):
+
+    matches_matcher = Raises()
+    def boom():
+        raise Exception('foo')
+    matches_matches = [boom]
+    matches_mismatches = [lambda:None]
+
+    # Tricky to get function objects to render constantly, and the interfaces
+    # helper uses assertEqual rather than (for instance) DocTestMatches.
+    str_examples = []
+
+    describe_examples = []
+
+
+class TestRaisesExceptionMatcherInterface(TestCase, TestMatchersInterface):
+
+    matches_matcher = Raises(
+        exception_matcher=MatchesException(Exception('foo')))
+    def boom_bar():
+        raise Exception('bar')
+    def boom_foo():
+        raise Exception('foo')
+    matches_matches = [boom_foo]
+    matches_mismatches = [lambda:None, boom_bar]
+
+    # Tricky to get function objects to render constantly, and the interfaces
+    # helper uses assertEqual rather than (for instance) DocTestMatches.
+    str_examples = []
+
+    describe_examples = []
+
+
+class TestRaisesBaseTypes(TestCase):
+
+    def raiser(self):
+        raise KeyboardInterrupt('foo')
+
+    def test_KeyboardInterrupt_matched(self):
+        # When KeyboardInterrupt is matched, it is swallowed.
+        matcher = Raises(MatchesException(KeyboardInterrupt))
+        self.assertThat(self.raiser, matcher)
+
+    def test_KeyboardInterrupt_propogates(self):
+        # The default 'it raised' propogates KeyboardInterrupt.
+        match_keyb = Raises(MatchesException(KeyboardInterrupt))
+        def raise_keyb_from_match():
+            matcher = Raises()
+            matcher.match(self.raiser)
+        self.assertThat(raise_keyb_from_match, match_keyb)
+    
+    def test_KeyboardInterrupt_match_Exception_propogates(self):
+        # If the raised exception isn't matched, and it is not a subclass of
+        # Exception, it is propogated.
+        match_keyb = Raises(MatchesException(KeyboardInterrupt))
+        def raise_keyb_from_match():
+            matcher = Raises(MatchesException(Exception))
+            matcher.match(self.raiser)
+        self.assertThat(raise_keyb_from_match, match_keyb)
+
+
 class DoesNotStartWithTests(TestCase):
 
     def test_describe(self):

=== modified file 'testtools/tests/test_monkey.py'
--- testtools/tests/test_monkey.py	2010-10-21 15:25:42 +0000
+++ testtools/tests/test_monkey.py	2010-11-11 09:52:45 +0000
@@ -4,6 +4,7 @@
 """Tests for testtools.monkey."""
 
 from testtools import TestCase
+from testtools.matchers import MatchesException, Raises
 from testtools.monkey import MonkeyPatcher, patch
 
 
@@ -137,8 +138,8 @@
         self.monkey_patcher.add_patch(self.test_object, 'foo', 'haha')
         self.monkey_patcher.add_patch(self.test_object, 'bar', 'blahblah')
 
-        self.assertRaises(
-            RuntimeError, self.monkey_patcher.run_with_patches, _)
+        self.assertThat(lambda:self.monkey_patcher.run_with_patches(_),
+            Raises(MatchesException(RuntimeError("Something went wrong!"))))
         self.assertEquals(self.test_object.foo, self.original_object.foo)
         self.assertEquals(self.test_object.bar, self.original_object.bar)
 

=== modified file 'testtools/tests/test_runtest.py'
--- testtools/tests/test_runtest.py	2010-10-25 16:42:32 +0000
+++ testtools/tests/test_runtest.py	2010-11-11 09:52:45 +0000
@@ -9,7 +9,7 @@
     TestCase,
     TestResult,
     )
-from testtools.matchers import Is
+from testtools.matchers import MatchesException, Is, Raises
 from testtools.tests.helpers import ExtendedTestResult
 
 
@@ -64,7 +64,8 @@
             raise KeyboardInterrupt("yo")
         run = RunTest(case, None)
         run.result = ExtendedTestResult()
-        self.assertRaises(KeyboardInterrupt, run._run_user, raises)
+        self.assertThat(lambda: run._run_user(raises),
+            Raises(MatchesException(KeyboardInterrupt)))
         self.assertEqual([], run.result._events)
 
     def test__run_user_calls_onException(self):
@@ -109,7 +110,8 @@
             log.append((result, err))
         run = RunTest(case, [(ValueError, log_exc)])
         run.result = ExtendedTestResult()
-        self.assertRaises(KeyError, run._run_user, raises)
+        self.assertThat(lambda: run._run_user(raises),
+            Raises(MatchesException(KeyError)))
         self.assertEqual([], run.result._events)
         self.assertEqual([], log)
 
@@ -128,7 +130,8 @@
             log.append((result, err))
         run = RunTest(case, [(ValueError, log_exc)])
         run.result = ExtendedTestResult()
-        self.assertRaises(ValueError, run._run_user, raises)
+        self.assertThat(lambda: run._run_user(raises),
+            Raises(MatchesException(ValueError)))
         self.assertEqual([], run.result._events)
         self.assertEqual([], log)
 
@@ -171,7 +174,8 @@
             raise Exception("foo")
         run = RunTest(case, lambda x: x)
         run._run_core = inner
-        self.assertRaises(Exception, run.run, result)
+        self.assertThat(lambda: run.run(result),
+            Raises(MatchesException(Exception("foo"))))
         self.assertEqual([
             ('startTest', case),
             ('stopTest', case),

=== modified file 'testtools/tests/test_spinner.py'
--- testtools/tests/test_spinner.py	2010-10-26 10:42:59 +0000
+++ testtools/tests/test_spinner.py	2010-11-11 09:52:45 +0000
@@ -12,6 +12,8 @@
 from testtools.matchers import (
     Equals,
     Is,
+    MatchesException,
+    Raises,
     )
 from testtools._spinner import (
     DeferredNotFired,
@@ -40,7 +42,7 @@
             calls.append(None)
             if len(calls) < 5:
                 log_something()
-        self.assertRaises(ReentryError, log_something)
+        self.assertThat(log_something, Raises(MatchesException(ReentryError)))
         self.assertEqual(1, len(calls))
 
     def test_deeper_stack(self):
@@ -55,7 +57,7 @@
             calls.append(None)
             if len(calls) < 5:
                 g()
-        self.assertRaises(ReentryError, f)
+        self.assertThat(f, Raises(MatchesException(ReentryError)))
         self.assertEqual(2, len(calls))
 
 
@@ -64,7 +66,8 @@
     def test_not_fired(self):
         # extract_result raises DeferredNotFired if it's given a Deferred that
         # has not fired.
-        self.assertRaises(DeferredNotFired, extract_result, defer.Deferred())
+        self.assertThat(lambda:extract_result(defer.Deferred()),
+            Raises(MatchesException(DeferredNotFired)))
 
     def test_success(self):
         # extract_result returns the value of the Deferred if it has fired
@@ -81,7 +84,8 @@
         except ZeroDivisionError:
             f = Failure()
         d = defer.fail(f)
-        self.assertRaises(ZeroDivisionError, extract_result, d)
+        self.assertThat(lambda:extract_result(d),
+            Raises(MatchesException(ZeroDivisionError)))
 
 
 class TestTrapUnhandledErrors(TestCase):
@@ -137,9 +141,9 @@
     def test_exception_reraised(self):
         # If the given function raises an error, run_in_reactor re-raises that
         # error.
-        self.assertRaises(
-            ZeroDivisionError,
-            self.make_spinner().run, self.make_timeout(), lambda: 1 / 0)
+        self.assertThat(
+            lambda:self.make_spinner().run(self.make_timeout(), lambda: 1/0),
+            Raises(MatchesException(ZeroDivisionError)))
 
     def test_keyword_arguments(self):
         # run_in_reactor passes keyword arguments on.
@@ -152,10 +156,9 @@
         # run_in_reactor raises an error if it is called inside another call
         # to run_in_reactor.
         spinner = self.make_spinner()
-        self.assertRaises(
-            ReentryError,
-            spinner.run, self.make_timeout(),
-            spinner.run, self.make_timeout(), lambda: None)
+        self.assertThat(lambda: spinner.run(
+            self.make_timeout(), spinner.run, self.make_timeout(), lambda: None),
+            Raises(MatchesException(ReentryError)))
 
     def test_deferred_value_returned(self):
         # If the given function returns a Deferred, run_in_reactor returns the
@@ -181,9 +184,9 @@
     def test_timeout(self):
         # If the function takes too long to run, we raise a TimeoutError.
         timeout = self.make_timeout()
-        self.assertRaises(
-            TimeoutError,
-            self.make_spinner().run, timeout, lambda: defer.Deferred())
+        self.assertThat(
+            lambda:self.make_spinner().run(timeout, lambda: defer.Deferred()),
+            Raises(MatchesException(TimeoutError)))
 
     def test_no_junk_by_default(self):
         # If the reactor hasn't spun yet, then there cannot be any junk.
@@ -259,8 +262,8 @@
         spinner = self.make_spinner(reactor)
         timeout = self.make_timeout()
         spinner.run(timeout, reactor.listenTCP, 0, ServerFactory())
-        self.assertRaises(
-            StaleJunkError, spinner.run, timeout, lambda: None)
+        self.assertThat(lambda: spinner.run(timeout, lambda: None),
+            Raises(MatchesException(StaleJunkError)))
 
     def test_clear_junk_clears_previous_junk(self):
         # If 'run' is called and there's still junk in the spinner's junk
@@ -284,8 +287,8 @@
         spinner = self.make_spinner(reactor)
         timeout = self.make_timeout()
         reactor.callLater(timeout, os.kill, os.getpid(), SIGINT)
-        self.assertRaises(
-            NoResultError, spinner.run, timeout * 5, defer.Deferred)
+        self.assertThat(lambda:spinner.run(timeout * 5, defer.Deferred),
+            Raises(MatchesException(NoResultError)))
         self.assertEqual([], spinner._clean())
 
     @skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only")
@@ -305,8 +308,8 @@
         spinner = self.make_spinner(reactor)
         timeout = self.make_timeout()
         reactor.callWhenRunning(os.kill, os.getpid(), SIGINT)
-        self.assertRaises(
-            NoResultError, spinner.run, timeout * 5, defer.Deferred)
+        self.assertThat(lambda:spinner.run(timeout * 5, defer.Deferred),
+            Raises(MatchesException(NoResultError)))
         self.assertEqual([], spinner._clean())
 
     @skipIf(os.name != "posix", "Sending SIGINT with os.kill is posix only")

=== modified file 'testtools/tests/test_testresult.py'
--- testtools/tests/test_testresult.py	2010-10-26 10:19:56 +0000
+++ testtools/tests/test_testresult.py	2010-11-11 09:52:45 +0000
@@ -36,7 +36,11 @@
     )
 from testtools.content import Content
 from testtools.content_type import ContentType, UTF8_TEXT
-from testtools.matchers import DocTestMatches
+from testtools.matchers import (
+    DocTestMatches,
+    MatchesException,
+    Raises,
+    )
 from testtools.tests.helpers import (
     LoggingResult,
     Python26TestResult,
@@ -760,8 +764,9 @@
 
     def test_outcome__no_details(self):
         self.make_extended_result()
-        self.assertRaises(ValueError,
-            getattr(self.converter, self.outcome), self)
+        self.assertThat(
+            lambda: getattr(self.converter, self.outcome)(self),
+            Raises(MatchesException(ValueError)))
 
 
 class TestExtendedToOriginalAddFailure(
@@ -821,8 +826,9 @@
 
     def test_outcome__no_details(self):
         self.make_extended_result()
-        self.assertRaises(ValueError,
-            getattr(self.converter, self.outcome), self)
+        self.assertThat(
+            lambda: getattr(self.converter, self.outcome)(self),
+            Raises(MatchesException(ValueError)))
 
 
 class TestExtendedToOriginalAddSuccess(

=== modified file 'testtools/tests/test_testtools.py'
--- testtools/tests/test_testtools.py	2010-10-28 20:18:39 +0000
+++ testtools/tests/test_testtools.py	2010-11-11 09:52:45 +0000
@@ -20,6 +20,8 @@
     )
 from testtools.matchers import (
     Equals,
+    MatchesException,
+    Raises,
     )
 from testtools.tests.helpers import (
     an_exc_info,
@@ -246,10 +248,9 @@
 
     def test_assertRaises_fails_when_different_error_raised(self):
         # assertRaises re-raises an exception that it didn't expect.
-        self.assertRaises(
-            ZeroDivisionError,
-            self.assertRaises,
-                RuntimeError, self.raiseError, ZeroDivisionError)
+        self.assertThat(lambda: self.assertRaises(RuntimeError,
+            self.raiseError, ZeroDivisionError),
+            Raises(MatchesException(ZeroDivisionError)))
 
     def test_assertRaises_returns_the_raised_exception(self):
         # assertRaises returns the exception object that was raised. This is
@@ -606,8 +607,8 @@
         def raiseKeyboardInterrupt():
             raise KeyboardInterrupt()
         self.test.addCleanup(raiseKeyboardInterrupt)
-        self.assertRaises(
-            KeyboardInterrupt, self.test.run, self.logging_result)
+        self.assertThat(lambda:self.test.run(self.logging_result),
+            Raises(MatchesException(KeyboardInterrupt)))
 
     def test_all_errors_from_MultipleExceptions_reported(self):
         # When a MultipleExceptions exception is caught, all the errors are
@@ -935,10 +936,12 @@
     """Tests for skipping of tests functionality."""
 
     def test_skip_causes_skipException(self):
-        self.assertRaises(self.skipException, self.skip, "Skip this test")
+        self.assertThat(lambda:self.skip("Skip this test"),
+            Raises(MatchesException(self.skipException)))
 
     def test_can_use_skipTest(self):
-        self.assertRaises(self.skipException, self.skipTest, "Skip this test")
+        self.assertThat(lambda:self.skipTest("Skip this test"),
+            Raises(MatchesException(self.skipException)))
 
     def test_skip_without_reason_works(self):
         class Test(TestCase):
@@ -1058,7 +1061,8 @@
         class Case(TestCase):
             def method(self):
                 self.addOnException(events.index)
-                self.assertRaises(ValueError, self.onException, an_exc_info)
+                self.assertThat(lambda: self.onException(an_exc_info),
+                    Raises(MatchesException(ValueError)))
         case = Case("method")
         case.run()
         self.assertThat(events, Equals([]))


Follow ups