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