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 | ... |
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__) |
Yay.