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

Proposed by Robert Collins
Status: Merged
Merged at revision: not available
Proposed branch: lp:~lifeless/testtools/addSkipped
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~lifeless/testtools/addSkipped
Reviewer Review Type Date Requested Status
Jonathan Lange Pending
Review via email: mp+4031@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

Yay.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'MANUAL'
2--- MANUAL 2008-10-04 03:51:34 +0000
3+++ MANUAL 2009-02-28 04:22:53 +0000
4@@ -24,6 +24,21 @@
5 ...
6
7
8+TestCase.skip
9+~~~~~~~~~~~~~
10+
11+``skip`` is a simple way to have a test stop running and be reported as a
12+skipped test, rather than a success/error/failure. This is an altnerative to
13+convoluted logic during test loading, permitting later and more localised
14+decisions about the appropriateness of running a test. Many reasons exist to
15+skip a test - for instance when a dependency is missing, or if the test is
16+expensive and should not be run while on laptop battery power, or if the test
17+is testing an incomplete feature (this is sometimes called a TODO). Using this
18+feature when running your test suite with a TestResult object that is missing
19+the ``addSkip`` method will result in the ``addError`` method being invoked
20+instead.
21+
22+
23 New assertion methods
24 ~~~~~~~~~~~~~~~~~~~~~
25
26@@ -60,6 +75,15 @@
27
28 ``testtools.clone_test_with_new_id`` is a function to copy a test case
29 instance to one with a new name. This is helpful for implementing test
30-paramterisation.
31-
32-
33+parameterisation.
34+
35+
36+Extensions to TestResult
37+------------------------
38+
39+TestResult.addSkip
40+~~~~~~~~~~~~~~~~~~
41+
42+This method is called on result objects when a test skips. The
43+``testtools.TestResult`` class records skips in its ``skip_reasons`` instance
44+dict. The can be reported on in much the same way as succesful tests.
45
46=== modified file 'testtools/testcase.py'
47--- testtools/testcase.py 2008-08-04 05:19:24 +0000
48+++ testtools/testcase.py 2009-02-28 04:22:53 +0000
49@@ -12,9 +12,15 @@
50 import unittest
51
52
53+class TestSkipped(Exception):
54+ """Raised within TestCase.run() when a test is skipped."""
55+
56+
57 class TestCase(unittest.TestCase):
58 """Extensions to the basic TestCase."""
59
60+ skipException = TestSkipped
61+
62 def __init__(self, *args, **kwargs):
63 unittest.TestCase.__init__(self, *args, **kwargs)
64 self._cleanups = []
65@@ -23,6 +29,19 @@
66 def shortDescription(self):
67 return self.id()
68
69+ def skip(self, reason):
70+ """Cause this test to be skipped.
71+
72+ This raises self.skipException(reason). skipException is raised
73+ to permit a skip to be triggered at any point(during setUp or the
74+ testMethod itself). The run() method catches skipException and
75+ translates that into a call to the result objects addSkip method.
76+
77+ :param reason: The reason why the test is being skipped. This must
78+ support being cast into a unicode string for reporting.
79+ """
80+ raise self.skipException(reason)
81+
82 def _formatTypes(self, classOrIterable):
83 """Format a class or a bunch of classes for display in an error."""
84 className = getattr(classOrIterable, '__name__', None)
85@@ -116,6 +135,18 @@
86 def runTest(self):
87 """Define this so we can construct a null test object."""
88
89+ def _handle_skip(self, result, reason):
90+ """Pass a skip to result.
91+
92+ If result has an addSkip method, this is called. If not, addError is
93+ called instead.
94+ """
95+ addSkip = getattr(result, 'addSkip', None)
96+ if not callable(addSkip):
97+ result.addError(self, self._exc_info())
98+ else:
99+ addSkip(self, reason)
100+
101 def run(self, result=None):
102 if result is None:
103 result = self.defaultTestResult()
104@@ -126,6 +157,10 @@
105 self.setUp()
106 except KeyboardInterrupt:
107 raise
108+ except self.skipException, e:
109+ self._handle_skip(result, e.args[0])
110+ self._runCleanups(result)
111+ return
112 except:
113 result.addError(self, self._exc_info())
114 self._runCleanups(result)
115@@ -135,6 +170,8 @@
116 try:
117 testMethod()
118 ok = True
119+ except self.skipException, e:
120+ self._handle_skip(result, e.args[0])
121 except self.failureException:
122 result.addFailure(self, self._exc_info())
123 except KeyboardInterrupt:
124
125=== modified file 'testtools/testresult.py'
126--- testtools/testresult.py 2008-10-04 03:59:24 +0000
127+++ testtools/testresult.py 2009-02-28 04:22:53 +0000
128@@ -12,6 +12,30 @@
129
130
131 class TestResult(unittest.TestResult):
132+ """Subclass of unittest.TestResult extending the protocol for flexability.
133+
134+ :ivar skip_reasons: A dict of skip-reasons -> list of tests. See addSkip.
135+ """
136+
137+ def __init__(self):
138+ super(TestResult, self).__init__()
139+ self.skip_reasons = {}
140+
141+ def addSkip(self, test, reason):
142+ """Called when a test has been skipped rather than running.
143+
144+ Like with addSuccess and addError, testStopped should still be called.
145+ addSkip is a separate method to addError for clarity, and to keep
146+ separate the internal details of how a given TestCase signals a skip,
147+ from how unexpected exceptions and failures are signalled.
148+
149+ :param test: The test that has been skipped.
150+ :param reason: The reason for the test being skipped. For instance,
151+ u"pyGL is not available".
152+ :return: None
153+ """
154+ skip_list = self.skip_reasons.setdefault(reason, [])
155+ skip_list.append(test)
156
157 def done(self):
158 """Called when the test runner is done."""
159@@ -40,6 +64,9 @@
160 def addFailure(self, test, failure):
161 self._dispatch('addFailure', test, failure)
162
163+ def addSkip(self, test, reason):
164+ self._dispatch('addSkip', test, reason)
165+
166 def addSuccess(self, test):
167 self._dispatch('addSuccess', test)
168
169
170=== modified file 'testtools/tests/helpers.py'
171--- testtools/tests/helpers.py 2008-10-04 03:51:34 +0000
172+++ testtools/tests/helpers.py 2009-02-28 04:22:53 +0000
173@@ -33,6 +33,10 @@
174 self._events.append(('addError', test, error))
175 super(LoggingResult, self).addError(test, error)
176
177+ def addSkip(self, test, reason):
178+ self._events.append(('addSkip', test, reason))
179+ super(LoggingResult, self).addSkip(test, reason)
180+
181 def addSuccess(self, test):
182 self._events.append(('addSuccess', test))
183 super(LoggingResult, self).addSuccess(test)
184
185=== modified file 'testtools/tests/test_testresult.py'
186--- testtools/tests/test_testresult.py 2008-10-04 03:59:24 +0000
187+++ testtools/tests/test_testresult.py 2009-02-28 04:22:53 +0000
188@@ -10,6 +10,27 @@
189 from testtools.tests.helpers import LoggingResult
190
191
192+class TestTestResultContract(TestCase):
193+ """Tests for the contract of TestResult's."""
194+
195+ def test_addSkipped(self):
196+ # Calling addSkip(test, reason) completes ok.
197+ result = self.makeResult()
198+ result.addSkip(self, u"Skipped for some reason")
199+
200+
201+class TestTestResultContract(TestTestResultContract):
202+
203+ def makeResult(self):
204+ return TestResult()
205+
206+
207+class TestMultiTestresultContract(TestTestResultContract):
208+
209+ def makeResult(self):
210+ return MultiTestResult(TestResult(), TestResult())
211+
212+
213 class TestTestResult(TestCase):
214 """Tests for `TestResult`."""
215
216@@ -17,6 +38,21 @@
217 """Make an arbitrary result for testing."""
218 return TestResult()
219
220+ def test_addSkipped(self):
221+ # Calling addSkip on a TestResult records the test that was skipped in
222+ # its skip_reasons dict.
223+ result = self.makeResult()
224+ result.addSkip(self, u"Skipped for some reason")
225+ self.assertEqual({u"Skipped for some reason":[self]},
226+ result.skip_reasons)
227+ result.addSkip(self, u"Skipped for some reason")
228+ self.assertEqual({u"Skipped for some reason":[self, self]},
229+ result.skip_reasons)
230+ result.addSkip(self, u"Skipped for another reason")
231+ self.assertEqual({u"Skipped for some reason":[self, self],
232+ u"Skipped for another reason":[self]},
233+ result.skip_reasons)
234+
235 def test_done(self):
236 # `TestResult` has a `done` method that, by default, does nothing.
237 self.makeResult().done()
238@@ -58,6 +94,13 @@
239 self.multiResult.stopTest(self)
240 self.assertResultLogsEqual([('stopTest', self)])
241
242+ def test_addSkipped(self):
243+ # Calling `addSkip` on a `MultiTestResult` calls addSkip on its
244+ # results.
245+ reason = u"Skipped for some reason"
246+ self.multiResult.addSkip(self, reason)
247+ self.assertResultLogsEqual([('addSkip', self, reason)])
248+
249 def test_addSuccess(self):
250 # Calling `addSuccess` on a `MultiTestResult` calls `addSuccess` on
251 # all its `TestResult`s.
252
253=== modified file 'testtools/tests/test_testtools.py'
254--- testtools/tests/test_testtools.py 2008-10-04 03:51:34 +0000
255+++ testtools/tests/test_testtools.py 2009-02-28 04:22:53 +0000
256@@ -2,6 +2,7 @@
257
258 """Tests for extensions to the base test library."""
259
260+import unittest
261 from testtools import TestCase, clone_test_with_new_id
262 from testtools.tests.helpers import LoggingResult
263
264@@ -375,6 +376,59 @@
265 "the original test instance should be unchanged.")
266
267
268+class TestSkipping(TestCase):
269+ """Tests for skipping of tests functionality."""
270+
271+ def test_skip_causes_skipException(self):
272+ self.assertRaises(self.skipException, self.skip, "Skip this test")
273+
274+ def test_skipException_in_setup_calls_result_addSkip(self):
275+ class TestThatRaisesInSetUp(TestCase):
276+ def setUp(self):
277+ self.skip("skipping this test")
278+ def test_that_passes(self):
279+ pass
280+ calls = []
281+ result = LoggingResult(calls)
282+ test = TestThatRaisesInSetUp("test_that_passes")
283+ test.run(result)
284+ self.assertEqual([('startTest', test),
285+ ('addSkip', test, "skipping this test"), ('stopTest', test)],
286+ calls)
287+
288+ def test_skipException_in_test_method_calls_result_addSkip(self):
289+ class SkippingTest(TestCase):
290+ def test_that_raises_skipException(self):
291+ self.skip("skipping this test")
292+ calls = []
293+ result = LoggingResult(calls)
294+ test = SkippingTest("test_that_raises_skipException")
295+ test.run(result)
296+ self.assertEqual([('startTest', test),
297+ ('addSkip', test, "skipping this test"), ('stopTest', test)],
298+ calls)
299+
300+ def test_skip__in_setup_with_old_result_object_calls_addError(self):
301+ class SkippingTest(TestCase):
302+ def setUp(self):
303+ raise self.skipException("skipping this test")
304+ def test_that_raises_skipException(self):
305+ pass
306+ result = unittest.TestResult()
307+ test = SkippingTest("test_that_raises_skipException")
308+ test.run(result)
309+ self.assertEqual(1, len(result.errors))
310+
311+ def test_skip_with_old_result_object_calls_addError(self):
312+ class SkippingTest(TestCase):
313+ def test_that_raises_skipException(self):
314+ raise self.skipException("skipping this test")
315+ result = unittest.TestResult()
316+ test = SkippingTest("test_that_raises_skipException")
317+ test.run(result)
318+ self.assertEqual(1, len(result.errors))
319+
320+
321 def test_suite():
322 from unittest import TestLoader
323 return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches