Merge lp:~mbp/testscenarios/mbp-doc into lp:~testtools-committers/testscenarios/trunk
- mbp-doc
- Merge into trunk
Proposed by
Martin Pool
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~mbp/testscenarios/mbp-doc |
Merge into: | lp:~testtools-committers/testscenarios/trunk |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~mbp/testscenarios/mbp-doc |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Collins | nice | Pending | |
Review via email: mp+4300@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'Makefile' |
2 | --- Makefile 2009-03-07 09:58:55 +0000 |
3 | +++ Makefile 2009-03-09 06:45:33 +0000 |
4 | @@ -1,9 +1,13 @@ |
5 | PYTHONPATH:=$(shell pwd)/lib:${PYTHONPATH} |
6 | +PYTHON ?= python |
7 | |
8 | all: |
9 | |
10 | +# it would be nice to use doctest directly to run the README, but that's |
11 | +# only supported from python2.6 onwards, so we need a script |
12 | check: |
13 | - PYTHONPATH=$(PYTHONPATH) python ./test_all.py $(TESTRULE) |
14 | + PYTHONPATH=$(PYTHONPATH):.:./lib $(PYTHON) run_doctest.py README |
15 | + PYTHONPATH=$(PYTHONPATH) $(PYTHON) ./test_all.py $(TESTRULE) |
16 | |
17 | clean: |
18 | find . -name '*.pyc' -print0 | xargs -0 rm -f |
19 | |
20 | === modified file 'README' |
21 | --- README 2009-03-08 04:37:33 +0000 |
22 | +++ README 2009-03-09 06:45:33 +0000 |
23 | @@ -1,5 +1,6 @@ |
24 | -testscenarios: extensions to python unittest to allow declarative |
25 | -dependency injection ('scenarios') by tests. |
26 | +***************************************************************** |
27 | +testscenarios: extensions to python unittest to support scenarios |
28 | +***************************************************************** |
29 | |
30 | Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> |
31 | |
32 | @@ -24,11 +25,13 @@ |
33 | dependencies externally to the test code itself, allowing easy testing in |
34 | different situations). |
35 | |
36 | -Dependencies: |
37 | -============= |
38 | +Dependencies |
39 | +============ |
40 | |
41 | * Python 2.4+ |
42 | -* testtools |
43 | +* testtools <https://launchpad.net/testtools> |
44 | + |
45 | + >>> import testtools |
46 | |
47 | |
48 | Why TestScenarios |
49 | @@ -51,19 +54,33 @@ |
50 | in multiple scenarios clear, easy to debug and work with even when the list |
51 | of scenarios is dynamically generated. |
52 | |
53 | -Getting Scenarios applied: |
54 | -========================== |
55 | + |
56 | +Defining Scenarios |
57 | +================== |
58 | + |
59 | +A **scenario** is a tuple of a string name for the scenario, and a dict of |
60 | +parameters describing the scenario. The name is appended to the test name, and |
61 | +the parameters are made available to the test instance when it's run. |
62 | + |
63 | +Scenarios are presented in **scenario lists** which are typically Python lists |
64 | +but may be any iterable. |
65 | + |
66 | + |
67 | +Getting Scenarios applied |
68 | +========================= |
69 | |
70 | At its heart the concept is simple. For a given test object with a list of |
71 | scenarios we prepare a new test object for each scenario. This involves: |
72 | - * Clone the test to a new test with a new id uniquely distinguishing it. |
73 | - * Apply the scenario to the test by setting each key, value in the scenario |
74 | - as attributes on the test object. |
75 | + |
76 | +* Clone the test to a new test with a new id uniquely distinguishing it. |
77 | +* Apply the scenario to the test by setting each key, value in the scenario |
78 | + as attributes on the test object. |
79 | |
80 | There are some complicating factors around making this happen seamlessly. These |
81 | factors are in two areas: |
82 | - * Choosing what scenarios to use. (See Setting Scenarios For A Test). |
83 | - * Getting the multiplication to happen. |
84 | + |
85 | +* Choosing what scenarios to use. (See Setting Scenarios For A Test). |
86 | +* Getting the multiplication to happen. |
87 | |
88 | Subclasssing |
89 | ++++++++++++ |
90 | @@ -84,10 +101,21 @@ |
91 | useful test base classes, or need to override run() or __call__ yourself) then |
92 | you can cause scenario application to happen later by calling |
93 | ``testscenarios.generate_scenarios()``. For instance:: |
94 | - >>> mytests = loader.loadTestsFromNames([...]) |
95 | - >>> test_suite = TestSuite() |
96 | + |
97 | + >>> import unittest |
98 | + >>> from testscenarios.scenarios import generate_scenarios |
99 | + |
100 | +This can work with loaders and runners from the standard library, or possibly other |
101 | +implementations:: |
102 | + |
103 | + >>> loader = unittest.TestLoader() |
104 | + >>> test_suite = unittest.TestSuite() |
105 | + >>> runner = unittest.TextTestRunner() |
106 | + |
107 | + >>> mytests = loader.loadTestsFromNames(['example.test_sample']) |
108 | >>> test_suite.addTests(generate_scenarios(mytests)) |
109 | >>> runner.run(test_suite) |
110 | + <unittest._TextTestResult run=1 errors=0 failures=0> |
111 | |
112 | Testloaders |
113 | +++++++++++ |
114 | @@ -100,35 +128,37 @@ |
115 | With ``load_tests``:: |
116 | |
117 | >>> def load_tests(standard_tests, module, loader): |
118 | - >>> result = loader.suiteClass() |
119 | - >>> result.addTests(generate_scenarios(standard_tests)) |
120 | - >>> return result |
121 | + ... result = loader.suiteClass() |
122 | + ... result.addTests(generate_scenarios(standard_tests)) |
123 | + ... return result |
124 | |
125 | With ``test_suite``:: |
126 | |
127 | >>> def test_suite(): |
128 | - >>> loader = TestLoader() |
129 | - >>> tests = loader.loadTestsFromName(__name__) |
130 | - >>> result = loader.suiteClass() |
131 | - >>> result.addTests(generate_scenarios(tests)) |
132 | - >>> return result |
133 | - |
134 | - |
135 | -Setting Scenarios for a test: |
136 | -============================= |
137 | + ... loader = TestLoader() |
138 | + ... tests = loader.loadTestsFromName(__name__) |
139 | + ... result = loader.suiteClass() |
140 | + ... result.addTests(generate_scenarios(tests)) |
141 | + ... return result |
142 | + |
143 | + |
144 | +Setting Scenarios for a test |
145 | +============================ |
146 | |
147 | A sample test using scenarios can be found in the doc/ folder. |
148 | |
149 | -See pydoc testscenarios for details. |
150 | +See `pydoc testscenarios` for details. |
151 | |
152 | On the TestCase |
153 | +++++++++++++++ |
154 | |
155 | You can set a scenarios attribute on the test case:: |
156 | |
157 | - >>> class MyTest(TestCase): |
158 | - >>> |
159 | - >>> scenarios = [scenario1, scenario2, ...] |
160 | + >>> class MyTest(unittest.TestCase): |
161 | + ... |
162 | + ... scenarios = [ |
163 | + ... ('scenario1', dict(param=1)), |
164 | + ... ('scenario2', dict(param=2)),] |
165 | |
166 | This provides the main interface by which scenarios are found for a given test. |
167 | Subclasses will inherit the scenarios (unless they override the attribute). |
168 | @@ -139,23 +169,41 @@ |
169 | Test scenarios can also be generated arbitrarily later, as long as the test has |
170 | not yet run. Simply replace (or alter, but be aware that many tests may share a |
171 | single scenarios attribute) the scenarios attribute. For instance in this |
172 | -example some third party tests are extended to run with a custom scenario. |
173 | +example some third party tests are extended to run with a custom scenario. :: |
174 | |
175 | - >>> for test in iterate_tests(stock_library_tests): |
176 | - >>> if isinstance(test, TestVFS): |
177 | - >>> test.scenarios = test.scenarios + [my_vfs_scenario] |
178 | - >>> ... |
179 | + >>> class TestTransport: |
180 | + ... """Hypothetical test case for bzrlib transport tests""" |
181 | + ... pass |
182 | + ... |
183 | + >>> stock_library_tests = unittest.TestLoader().loadTestsFromNames( |
184 | + ... ['example.test_sample']) |
185 | + ... |
186 | + >>> for test in testtools.iterate_tests(stock_library_tests): |
187 | + ... if isinstance(test, TestTransport): |
188 | + ... test.scenarios = test.scenarios + [my_vfs_scenario] |
189 | + ... |
190 | + >>> suite = unittest.TestSuite() |
191 | >>> suite.addTests(generate_scenarios(stock_library_tests)) |
192 | |
193 | -Note that adding scenarios to a test that has already been parameterised via |
194 | -generate_scenarios generates a cross product:: |
195 | - >>> class CrossProductDemo(TestCase): |
196 | - >>> scenarios = [scenario_0_0, scenario_0_1] |
197 | - >>> def test_foo(self): |
198 | - >>> return |
199 | +Generated tests don't have a ``scenarios`` list, because they don't normally |
200 | +require any more expansion. However, you can add a ``scenarios`` list back on |
201 | +to them, and then run them through ``generate_scenarios`` again to generate the |
202 | +cross product of tests. :: |
203 | + |
204 | + >>> class CrossProductDemo(unittest.TestCase): |
205 | + ... scenarios = [('scenario_0_0', {}), |
206 | + ... ('scenario_0_1', {})] |
207 | + ... def test_foo(self): |
208 | + ... return |
209 | + ... |
210 | + >>> suite = unittest.TestSuite() |
211 | >>> suite.addTests(generate_scenarios(CrossProductDemo("test_foo"))) |
212 | - >>> for test in iterate_tests(suite): |
213 | - >>> test.scenarios = test.scenarios + [scenario_1_0, scenario_1_1] |
214 | + >>> for test in testtools.iterate_tests(suite): |
215 | + ... test.scenarios = [ |
216 | + ... ('scenario_1_0', {}), |
217 | + ... ('scenario_1_1', {})] |
218 | + ... |
219 | + >>> suite2 = unittest.TestSuite() |
220 | >>> suite2.addTests(generate_scenarios(suite)) |
221 | >>> print suite2.countTestCases() |
222 | 4 |
223 | @@ -168,27 +216,28 @@ |
224 | scenarios somewhere relevant to the tests that will use it, and then that can |
225 | be customised, or dynamically populate your scenarios from a registry etc. |
226 | For instance:: |
227 | + |
228 | >>> hash_scenarios = [] |
229 | >>> try: |
230 | - >>> import md5 |
231 | - >>> except ImportError: |
232 | - >>> pass |
233 | - >>> else: |
234 | - >>> hash_scenarios.append(("md5", "hash": md5.new)) |
235 | + ... from hashlib import md5 |
236 | + ... except ImportError: |
237 | + ... pass |
238 | + ... else: |
239 | + ... hash_scenarios.append(("md5", dict(hash=md5))) |
240 | >>> try: |
241 | - >>> import sha1 |
242 | - >>> except ImportError: |
243 | - >>> pass |
244 | - >>> else: |
245 | - >>> hash_scenarios.append(("sha1", "hash": sha1.new)) |
246 | - >>> |
247 | - >>> class TestHashContract(TestCase): |
248 | - >>> |
249 | - >>> scenarios = hash_scenarios |
250 | - >>> |
251 | - >>> class TestHashPerformance(TestCase): |
252 | - >>> |
253 | - >>> scenarios = hash_scenarios |
254 | + ... from hashlib import sha1 |
255 | + ... except ImportError: |
256 | + ... pass |
257 | + ... else: |
258 | + ... hash_scenarios.append(("sha1", dict(hash=sha1))) |
259 | + ... |
260 | + >>> class TestHashContract(unittest.TestCase): |
261 | + ... |
262 | + ... scenarios = hash_scenarios |
263 | + ... |
264 | + >>> class TestHashPerformance(unittest.TestCase): |
265 | + ... |
266 | + ... scenarios = hash_scenarios |
267 | |
268 | |
269 | Forcing Scenarios |
270 | @@ -201,3 +250,10 @@ |
271 | ``apply_scenarios`` function does not reset the test scenarios attribute, |
272 | allowing it to be used to layer scenarios without affecting existing scenario |
273 | selection. |
274 | + |
275 | + |
276 | +Advice on Writing Scenarios |
277 | +=========================== |
278 | + |
279 | +If a parameterised test is because of a bug run without being parameterized, |
280 | +it should fail rather than running with defaults, because this can hide bugs. |
281 | |
282 | === added directory 'example' |
283 | === added file 'example/__init__.py' |
284 | --- example/__init__.py 1970-01-01 00:00:00 +0000 |
285 | +++ example/__init__.py 2009-03-09 06:45:33 +0000 |
286 | @@ -0,0 +1,1 @@ |
287 | +# contractual obligation |
288 | |
289 | === added file 'example/test_sample.py' |
290 | --- example/test_sample.py 1970-01-01 00:00:00 +0000 |
291 | +++ example/test_sample.py 2009-03-09 06:45:33 +0000 |
292 | @@ -0,0 +1,6 @@ |
293 | +import unittest |
294 | + |
295 | +class TestSample(unittest.TestCase): |
296 | + |
297 | + def test_so_easy(self): |
298 | + pass |
299 | |
300 | === added file 'run_doctest.py' |
301 | --- run_doctest.py 1970-01-01 00:00:00 +0000 |
302 | +++ run_doctest.py 2009-03-09 06:45:33 +0000 |
303 | @@ -0,0 +1,5 @@ |
304 | +import doctest |
305 | +import sys |
306 | + |
307 | +for n in sys.argv: |
308 | + print doctest.testfile(n, raise_on_error=False, ) |
Documentation is now more correct and tested