Merge lp:~lifeless/testtools/that into lp:~testtools-committers/testtools/trunk

Proposed by Robert Collins
Status: Merged
Approved by: Jonathan Lange
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~lifeless/testtools/that
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 317 lines (+221/-0)
8 files modified
MANUAL (+21/-0)
NEWS (+5/-0)
testtools/__init__.py (+3/-0)
testtools/matchers.py (+89/-0)
testtools/testcase.py (+13/-0)
testtools/tests/__init__.py (+2/-0)
testtools/tests/test_matchers.py (+59/-0)
testtools/tests/test_testtools.py (+29/-0)
To merge this branch: bzr merge lp:~lifeless/testtools/that
Reviewer Review Type Date Requested Status
Jonathan Lange Approve
Review via email: mp+15070@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

This adds the assertThat hamcrest style matcher that I have been babbling about for a while. I'm using it for testing the output of the TextTestResult that I'm prepping.

lp:~lifeless/testtools/that updated
30. By Robert Collins

Add docs.

Revision history for this message
Jonathan Lange (jml) wrote :

Hey Rob,

This is awesome. I'm making a small punctuation change to one of the docs and landing this.

jml

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'MANUAL'
2--- MANUAL 2009-04-16 04:44:31 +0000
3+++ MANUAL 2009-11-20 00:58:08 +0000
4@@ -49,6 +49,7 @@
5 * assertIs
6 * assertIsNot
7 * assertIsInstance
8+ * assertThat
9
10
11 Improved assertRaises
12@@ -62,6 +63,26 @@
13 self.assertEqual('User bob cannot frobnicate', str(error))
14
15
16+TestCase.assertThat
17+~~~~~~~~~~~~~~~~~~~
18+
19+assertThat is a clean way to write complex assertions without tying them to
20+the TestCase inheritance hierarchy (and thus making them easier to reuse).
21+
22+assertThat takes an object to be matched, and a matcher, and fails if the
23+matcher does not match the matchee.
24+
25+See pydoc testtools.Matcher for the protocol that matchers need to implement.
26+
27+testtools includes some matchers in testtools.matchers.
28+
29+An example using the DocTestMatches matcher which uses doctests example
30+matching logic::
31+
32+ def test_foo(self):
33+ self.assertThat([1,2,3,4], DocTestMatches('[1, 2, 3, 4]'))
34+
35+
36 Creation methods
37 ~~~~~~~~~~~~~~~~
38
39
40=== modified file 'NEWS'
41--- NEWS 2009-11-10 22:37:05 +0000
42+++ NEWS 2009-11-20 00:58:08 +0000
43@@ -12,6 +12,11 @@
44 of becoming an upstream Python API. For more details see pydoc
45 testtools.TestResult and the TestCase addDetail / getDetails methods.
46
47+* assertThat has been added to TestCase. This new assertion supports
48+ a hamcrest inspired matching protocol. See pydoc testtools.Matcher for
49+ details about writing matchers, and testtools.matchers for the included
50+ matchers.
51+
52 * Compatible with Python 2.6 and Python 2.7
53
54 * Failing to upcall in setUp or tearDown will now cause a test failure.
55
56=== modified file 'testtools/__init__.py'
57--- testtools/__init__.py 2009-10-28 11:00:27 +0000
58+++ testtools/__init__.py 2009-11-20 00:58:08 +0000
59@@ -16,6 +16,9 @@
60 'ThreadsafeForwardingResult',
61 ]
62
63+from testtools.matchers import (
64+ Matcher,
65+ )
66 from testtools.testcase import (
67 TestCase,
68 clone_test_with_new_id,
69
70=== added file 'testtools/matchers.py'
71--- testtools/matchers.py 1970-01-01 00:00:00 +0000
72+++ testtools/matchers.py 2009-11-20 00:58:08 +0000
73@@ -0,0 +1,89 @@
74+# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
75+
76+"""Matchers, a way to express complex assertions outside the testcase.
77+
78+Inspired by 'hamcrest'.
79+
80+Matcher provides the abstract API that all matchers need to implement.
81+
82+Bundled matchers are listed in __all__: a list can be obtained by running
83+$ python -c 'import testtools.matchers; print testtools.matchers.__all__'
84+"""
85+
86+__metaclass__ = type
87+__all__ = [
88+ 'DocTestMatches',
89+ ]
90+
91+import doctest
92+
93+
94+class Matcher:
95+ """A pattern matcher.
96+
97+ A Matcher must implement matches, __str__ and describe_difference to be
98+ used by testtools.TestCase.assertThat.
99+
100+ Matchers can be useful outside of test cases, as they are simply a
101+ pattern matching language expressed as objects.
102+
103+ testtools.matchers is inspired by hamcrest, but is pythonic rather than
104+ a Java transcription.
105+ """
106+
107+ def matches(self, something):
108+ """Returns True if this matcher matches something, False otherwise."""
109+ raise NotImplementedError(self.matches)
110+
111+ def __str__(self):
112+ """Get a sensible human representation of the matcher.
113+
114+ This should include the parameters given to the matcher and any
115+ state that would affect the matches operation.
116+ """
117+ raise NotImplementedError(self.__str__)
118+
119+ def describe_difference(self, something):
120+ """Describe why something did not match.
121+
122+ This should be either human readable or castable to a string.
123+ """
124+ raise NotImplementedError(self.describe_difference)
125+
126+
127+class DocTestMatches:
128+ """See if a string matches a doctest example."""
129+
130+ def __init__(self, example, flags=0):
131+ """Create a DocTestMatches to match example.
132+
133+ :param example: The example to match e.g. 'foo bar baz'
134+ :param flags: doctest comparison flags to match on. e.g.
135+ doctest.ELLIPSIS.
136+ """
137+ if not example.endswith('\n'):
138+ example += '\n'
139+ self.want = example # required variable name by doctest.
140+ self.flags = flags
141+ self._checker = doctest.OutputChecker()
142+
143+ def __str__(self):
144+ if self.flags:
145+ flagstr = ", flags=%d" % self.flags
146+ else:
147+ flagstr = ""
148+ return 'DocTestMatches(%r%s)' % (self.want, flagstr)
149+
150+ def _with_nl(self, actual):
151+ result = str(actual)
152+ if not result.endswith('\n'):
153+ result += '\n'
154+ return result
155+
156+ def matches(self, actual):
157+ return self._checker.check_output(self.want, self._with_nl(actual),
158+ self.flags)
159+
160+ def describe_difference(self, actual):
161+ return self._checker.output_difference(self, self._with_nl(actual),
162+ self.flags)
163
164=== modified file 'testtools/testcase.py'
165--- testtools/testcase.py 2009-11-11 08:53:54 +0000
166+++ testtools/testcase.py 2009-11-20 00:58:08 +0000
167@@ -184,6 +184,18 @@
168 self.fail("%s not raised, %r returned instead." % (excName, ret))
169 failUnlessRaises = assertRaises
170
171+ def assertThat(self, matchee, matcher):
172+ """Assert that matchee is matched by matcher.
173+
174+ :param matchee: An object to match with matcher.
175+ :param matcher: An object meeting the testtools.Matcher protocol.
176+ :raises self.failureException: When matcher does not match thing.
177+ """
178+ if matcher.matches(matchee):
179+ return
180+ self.fail('Match failed. Matchee: "%s"\nMatcher: %s\nDifference: %s\n'
181+ % (matchee, matcher, matcher.describe_difference(matchee)))
182+
183 def expectFailure(self, reason, predicate, *args, **kwargs):
184 """Check that a test fails in a particular way.
185
186@@ -301,6 +313,7 @@
187 result.addSuccess(self, details=self.getDetails())
188 finally:
189 result.stopTest(self)
190+ return result
191
192 def setUp(self):
193 unittest.TestCase.setUp(self)
194
195=== modified file 'testtools/tests/__init__.py'
196--- testtools/tests/__init__.py 2009-10-28 10:29:30 +0000
197+++ testtools/tests/__init__.py 2009-11-20 00:58:08 +0000
198@@ -4,6 +4,7 @@
199 from testtools.tests import (
200 test_content,
201 test_content_type,
202+ test_matchers,
203 test_testtools,
204 test_testresult,
205 test_testsuite,
206@@ -15,6 +16,7 @@
207 modules = [
208 test_content,
209 test_content_type,
210+ test_matchers,
211 test_testresult,
212 test_testsuite,
213 test_testtools,
214
215=== added file 'testtools/tests/test_matchers.py'
216--- testtools/tests/test_matchers.py 1970-01-01 00:00:00 +0000
217+++ testtools/tests/test_matchers.py 2009-11-20 00:58:08 +0000
218@@ -0,0 +1,59 @@
219+# Copyright (c) 2008 Jonathan M. Lange. See LICENSE for details.
220+
221+"""Tests for matchers."""
222+
223+import doctest
224+
225+from testtools import (
226+ Matcher, # check that Matcher is exposed at the top level for docs.
227+ TestCase,
228+ )
229+from testtools.matchers import (
230+ DocTestMatches,
231+ )
232+
233+
234+class TestDocTestMatchesInterface(TestCase):
235+
236+ def test_matches_matches(self):
237+ matcher = DocTestMatches("Ran 1 test in ...s", doctest.ELLIPSIS)
238+ matches = ["Ran 1 test in 0.000s", "Ran 1 test in 1.234s"]
239+ mismatches = ["Ran 1 tests in 0.000s", "Ran 2 test in 0.000s"]
240+ for candidate in matches:
241+ self.assertTrue(matcher.matches(candidate))
242+ for candidate in mismatches:
243+ self.assertFalse(matcher.matches(candidate))
244+
245+ def test__str__(self):
246+ # [(expected, object to __str__)].
247+ examples = [("DocTestMatches('Ran 1 test in ...s\\n')",
248+ DocTestMatches("Ran 1 test in ...s")),
249+ ("DocTestMatches('foo\\n', flags=8)", DocTestMatches("foo", flags=8)),
250+ ]
251+ for expected, matcher in examples:
252+ self.assertThat(matcher, DocTestMatches(expected))
253+
254+ def test_describe_difference(self):
255+ # [(expected, matchee, matcher), ...]
256+ examples = [('Expected:\n Ran 1 test in ...s\nGot:\n'
257+ ' Ran 1 test in 0.123s\n', "Ran 1 test in 0.123s",
258+ DocTestMatches("Ran 1 test in ...s", doctest.ELLIPSIS))]
259+ for difference, matchee, matcher in examples:
260+ self.assertEqual(difference, matcher.describe_difference(matchee))
261+
262+
263+class TestDocTestMatchesSpecific(TestCase):
264+
265+ def test___init__simple(self):
266+ matcher = DocTestMatches("foo")
267+ self.assertEqual("foo\n", matcher.want)
268+
269+ def test___init__flags(self):
270+ matcher = DocTestMatches("bar\n", doctest.ELLIPSIS)
271+ self.assertEqual("bar\n", matcher.want)
272+ self.assertEqual(doctest.ELLIPSIS, matcher.flags)
273+
274+
275+def test_suite():
276+ from unittest import TestLoader
277+ return TestLoader().loadTestsFromName(__name__)
278
279=== modified file 'testtools/tests/test_testtools.py'
280--- testtools/tests/test_testtools.py 2009-11-10 23:14:42 +0000
281+++ testtools/tests/test_testtools.py 2009-11-20 00:58:08 +0000
282@@ -233,6 +233,35 @@
283 self.assertFails(
284 '[42] is [42]', self.assertIsNot, some_list, some_list)
285
286+ def test_assertThat_matches_clean(self):
287+ class Matcher:
288+ def matches(self, foo):
289+ return True
290+ self.assertThat("foo", Matcher())
291+
292+ def test_assertThat_mismatch_raises_description(self):
293+ calls = []
294+ class Matcher:
295+ def matches(self, thing):
296+ calls.append(('match', thing))
297+ return False
298+ def __str__(self):
299+ calls.append(('__str__',))
300+ return "a description"
301+ def describe_difference(self, thing):
302+ calls.append(('describe_diff', thing))
303+ return "object is not a thing"
304+ class Test(TestCase):
305+ def test(self):
306+ self.assertThat("foo", Matcher())
307+ result = Test("test").run()
308+ self.assertEqual([
309+ ('match', "foo"),
310+ ('describe_diff', "foo"),
311+ ('__str__',),
312+ ], calls)
313+ self.assertFalse(result.wasSuccessful())
314+
315
316 class TestAddCleanup(TestCase):
317 """Tests for TestCase.addCleanup."""

Subscribers

People subscribed via source and target branches