Merge ~cjwatson/launchpad:tests-style-guide into launchpad:master
- Git
- lp:~cjwatson/launchpad
- tests-style-guide
- Merge into master
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | cc3288df209fc6409e550828909d595e9cf0db36 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:tests-style-guide |
Merge into: | launchpad:master |
Prerequisite: | ~cjwatson/launchpad:dev-guides-formatting |
Diff against target: |
556 lines (+527/-1) 3 files modified
doc/guides/python.rst (+1/-1) doc/guides/tests.rst (+525/-0) doc/index.rst (+1/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jürgen Gmach | Approve | ||
Review via email: mp+411739@code.launchpad.net |
Commit message
Add tests style guide from dev.launchpad.net
Description of the change
This was previously https:/
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
Copy and paste strikes again. Thanks for catching that.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/doc/guides/python.rst b/doc/guides/python.rst |
2 | index c426bd6..be09dfe 100644 |
3 | --- a/doc/guides/python.rst |
4 | +++ b/doc/guides/python.rst |
5 | @@ -34,7 +34,7 @@ Related Documents |
6 | * `Exception guidelines <https://dev.launchpad.net/ExceptionGuidelines>`_ |
7 | * `Assertions <https://dev.launchpad.net/AssertionsInLaunchpad>`_ |
8 | * `Launchpad hacking FAQ <https://dev.launchpad.net/LaunchpadHackingFAQ>`_ |
9 | -* `Tests style guide <https://dev.launchpad.net/TestsStyleGuide>`_ |
10 | +* :doc:`Tests style guide <tests>` |
11 | |
12 | Whitespace and Wrapping |
13 | ======================= |
14 | diff --git a/doc/guides/tests.rst b/doc/guides/tests.rst |
15 | new file mode 100644 |
16 | index 0000000..0615ae6 |
17 | --- /dev/null |
18 | +++ b/doc/guides/tests.rst |
19 | @@ -0,0 +1,525 @@ |
20 | +================= |
21 | +Tests Style Guide |
22 | +================= |
23 | + |
24 | +This page documents conventions for our Launchpad tests. Reviewers make |
25 | +sure that code merged is documented and covered by tests. Following the |
26 | +principles outlined in this document will minimize comments related to test |
27 | +style from reviewers. |
28 | + |
29 | +Reviewers will block merge of code that is under-documented or under-tested. |
30 | +We have two primary means of documentation: |
31 | + |
32 | +#. System documentation in ``doc`` directories such as ``lib/lp/<app>/doc``. |
33 | +#. Page tests in ``lib/lp/<app>/stories``. |
34 | +#. View tests in ``lib/lp/<app>/browser/tests``. |
35 | + |
36 | +While these two types of documentation use the doctest format, which means |
37 | +that they contain testable examples, they are documentation first. So they |
38 | +are not the best place to test many corner cases or various similar |
39 | +possibilities. This is best done in other unit tests or functional tests, |
40 | +which have ensuring complete test coverage as their main objective. |
41 | + |
42 | +Testable Documentation |
43 | +====================== |
44 | + |
45 | +Testable documentation includes system documentation doctests and page |
46 | +tests. |
47 | + |
48 | +What do we mean by "testable documentation"? One rule of thumb is the |
49 | +narrative should be similar to what you'd see in an API reference manual. |
50 | +Another trick to ensure the narrative is appropriately detailed is to |
51 | +mentally remove all of the code snippets and see if the narrative stands by |
52 | +itself. |
53 | + |
54 | +System Documentation |
55 | +-------------------- |
56 | + |
57 | +These are doctests located under ``lib/lp/<app>/doc``. They are used to |
58 | +document the APIs and other internal objects. The documentation should |
59 | +explain to a developer how to use these objects and what purpose they serve. |
60 | + |
61 | +Each modification to ``lp.<app>.interfaces`` should be documented in one of |
62 | +these files. |
63 | + |
64 | +(Each file in that directory is automatically added to the test suite. If |
65 | +you need to configure the test layer in which the test will be run or need |
66 | +to customize the test fixture, you can add special instructions for the file |
67 | +in the system documentation harness in ``lib/lp/<app>/tests/test_doc.py``.) |
68 | + |
69 | +Use Cases Documentation: Page Tests |
70 | +----------------------------------- |
71 | + |
72 | +We use page tests to document all the use cases that Launchpad satisfies. |
73 | +The narrative in these files should document the use case. That is, they |
74 | +should explain what the user's objective is and how he accomplishes it. |
75 | + |
76 | +The examples in these files uses ``zope.testbrowser`` to show how the user |
77 | +would navigate the workflow relevant to the use case. |
78 | + |
79 | +So each addition to the UI should be covered by an appropriate section in a |
80 | +page test. |
81 | + |
82 | +The page tests do not need to document and demonstrate each and every |
83 | +possible way to navigate the workflow. This can usually be done in a more |
84 | +direct manner by testing the view object directly. |
85 | + |
86 | +Browser View Tests |
87 | +------------------ |
88 | + |
89 | +View objects are usually documented that way along other system objects in |
90 | +files named ``*-pages.txt`` or in |
91 | +``lib/lp/<app>/browser/tests/*-views.txt``. |
92 | + |
93 | +The browser tests directory contains both doctest files for documenting the |
94 | +use of browser view classes and unit tests (e.g. ``test_*.py``) for |
95 | +performing unit tests, including covering corner cases. Currently some |
96 | +apps, registry for example, have a large number of doctests in this location |
97 | +that are not strictly testable documentation. Over time these |
98 | +non-documentation doctest files should be converted to unit tests. |
99 | + |
100 | +**All new browser tests that are not testable documentation should be |
101 | +written as unit tests.** |
102 | + |
103 | +Common Conventions |
104 | +------------------ |
105 | + |
106 | +The basic conventions for testable documentation are: |
107 | + |
108 | +* Example code is wrapped at 78 columns, follows the regular :doc:`Python |
109 | + style guide <python>`, and is indented 4 spaces. |
110 | +* Narrative text may be wrapped at either 72 or 78 columns. |
111 | +* You can use regular Python comments for explanations related to the code |
112 | + and not to the documentation. |
113 | +* Doctests use Restructured Text (or "ReST", see |
114 | + http://docutils.sourceforge.net/docs/user/rst/quickref.html). |
115 | + |
116 | + * We use ReST because it's what the Python community have standardized on |
117 | + and because it makes setting up Sphinx to browse all the doctests |
118 | + simpler. |
119 | + |
120 | +* The file should have a first-level title element. An expansion of the |
121 | + filename is usually a good start. For example, the file |
122 | + ``bugcomment.txt`` could have this title: |
123 | + |
124 | +.. code-block:: rst |
125 | + |
126 | + Bug Comments |
127 | + ============ |
128 | + |
129 | +* Two blank lines are used to separate the start of a new section (a header). |
130 | + |
131 | +.. code-block:: rst |
132 | + |
133 | + An Example |
134 | + ---------- |
135 | + |
136 | + Launchpad tracks foo and bar elements using the IFooBarSet utility. |
137 | + |
138 | + >>> from lp.foo.interfaces.bar import IBar, IFoo, IFooBarSet |
139 | + >>> from lp.testing import verifyObject |
140 | + >>> foobarset = getUtility(IFooBarSet) |
141 | + |
142 | + >>> verifyObject(IFooBarSet, foobarset) |
143 | + True |
144 | + |
145 | + You use the getFoo() method to obtain an IFoo instance by id: |
146 | + |
147 | + >>> foo = foobarset.getFoo('aFoo') |
148 | + >>> verifyObject(IFoo, foo) |
149 | + True |
150 | + |
151 | + Similarly, you use the getBar() method to retrieve an IBar instance by |
152 | + id: |
153 | + |
154 | + >>> bar = foobarset.getBar('aBar') |
155 | + >>> verifyObject(IBar, bar) |
156 | + True |
157 | + |
158 | +Each individual test should be of the form: |
159 | + |
160 | +.. code-block:: pycon |
161 | + |
162 | + >>> do_something() |
163 | + expected output |
164 | + |
165 | +This means that something like this isn't considered a test, but test setup |
166 | +(since it doesn't produce any output): |
167 | + |
168 | +.. code-block:: pycon |
169 | + |
170 | + >>> do_something() |
171 | + |
172 | +For the reason above, the assert statement shouldn't be used in doctests. |
173 | + |
174 | +Comparing Results |
175 | +----------------- |
176 | + |
177 | +When writing doctests, make sure that if the test fails, the failure message |
178 | +will be helpful to debug the problem. Avoid constructs like: |
179 | + |
180 | +.. code-block:: pycon |
181 | + |
182 | + >>> 'Test' in foo.getText() |
183 | + True |
184 | + |
185 | +The failure message for this test will be: |
186 | + |
187 | +.. code-block:: pycon |
188 | + |
189 | + - True |
190 | + + False |
191 | + |
192 | +which isn't helpful at all in understanding what went wrong. This |
193 | +example is a lot more helpful when it fails: |
194 | + |
195 | +.. code-block:: pycon |
196 | + |
197 | + >>> foo.getText() |
198 | + '...Test...' |
199 | + |
200 | +For page tests where the page contains a lot of elements, you should zoom in |
201 | +to the relevant part. You can use the ``find_main_content()``, |
202 | +``find_tags_by_class()``, ``find_tag_by_id()``, and ``find_portlet()`` |
203 | +helper methods. They return ``BeautifulSoup`` instances, which makes it |
204 | +easy to access specific elements in the tree. |
205 | + |
206 | +.. code-block:: rst |
207 | + |
208 | + The new status is displayed in the portlet. |
209 | + |
210 | + >>> details_portlet = find_portlet(browser.contents, 'Question details') |
211 | + >>> print(details_portlet.find('b', text='Status:').next.strip()) |
212 | + Needs information |
213 | + |
214 | +There is also an ``extract_text()`` helper that only renders the HTML text: |
215 | + |
216 | +.. code-block:: pycon |
217 | + |
218 | + >>> print(extract_text( |
219 | + ... find_tag_by_id(browser.contents, 'branchtable'))) |
220 | + main 60 New firefox |
221 | + klingon 30 Experimental gnome-terminal |
222 | + junk.contrib 60 New 2005-10-31 12:03:57 ... weeks ago |
223 | + |
224 | +Read `Page tests <https://dev.launchpad.net/PageTests>`_ for other tips on |
225 | +writing page tests. |
226 | + |
227 | +When to print and when to return values |
228 | +--------------------------------------- |
229 | + |
230 | +Doctests mimic the Python interactive interpreter, so generally it's |
231 | +preferred to simply return values and expect to see their string |
232 | +representation. In a few cases though, it's better to ``print`` the results |
233 | +instead of just returning them. |
234 | + |
235 | +The two most common cases of this are ``None`` and strings. The interactive |
236 | +interpreter suppresses ``None`` return values, so relying on these means the |
237 | +doctest makes less sense. You could compare against ``None``, but the |
238 | +``True`` or ``False`` output isn't explicit, so it's almost always better to |
239 | +print values you expect to be ``None``. |
240 | + |
241 | +Instead of: |
242 | + |
243 | +.. code-block:: pycon |
244 | + |
245 | + >>> should_be_none() |
246 | + >>> do_something_else() |
247 | + |
248 | +Use: |
249 | + |
250 | +.. code-block:: pycon |
251 | + |
252 | + >>> print(should_be_none()) |
253 | + None |
254 | + >>> do_something_else() |
255 | + |
256 | +For a different reason, it's also usually better to print string results |
257 | +rather than just returning them. Returning the string causes the quotes to |
258 | +be included in the output, while printing the string does not. Those extra |
259 | +quotes are usually noise. |
260 | + |
261 | +Instead of: |
262 | + |
263 | +.. code-block:: pycon |
264 | + |
265 | + >>> get_some_text() |
266 | + 'foo' |
267 | + >>> get_some_string() |
268 | + "Don't care" |
269 | + |
270 | +Use: |
271 | + |
272 | +.. code-block:: pycon |
273 | + |
274 | + >>> print(get_some_text()) |
275 | + foo |
276 | + >>> print(get_some_string()) |
277 | + Don't care |
278 | + |
279 | +Dictionaries and sets |
280 | +--------------------- |
281 | + |
282 | +You can't just print the value of a dictionary or a set when that collection |
283 | +has more than one element in it, e.g. |
284 | + |
285 | +.. code-block:: pycon |
286 | + |
287 | + >>> print(my_dict) |
288 | + {'a': 1, 'b': 2} |
289 | + |
290 | +The reason is that Python does not guarantee the order of its elements in a |
291 | +dictionary or set, so the printed representation of a dictionary is |
292 | +indeterminate. In page tests, there's a ``pretty()`` global which is |
293 | +basically exposing Python's pretty printer, and you can use it safely: |
294 | + |
295 | +.. code-block:: pycon |
296 | + |
297 | + >>> pretty(my_dict) |
298 | + {'a': 1, 'b': 2} |
299 | + |
300 | +Though it's a bit uglier, you can also print the sorted items of a |
301 | +dictionary: |
302 | + |
303 | +.. code-block:: pycon |
304 | + |
305 | + >>> sorted(my_dict.items()) |
306 | + [('a', 1), ('b', 2)] |
307 | + |
308 | +Global State |
309 | +------------ |
310 | + |
311 | +Be especially careful of test code that changes global state. For example, |
312 | +we were recently bitten by code in a test that did this: |
313 | + |
314 | +.. code-block:: python |
315 | + |
316 | + socket.setdefaulttimeout(1) |
317 | + |
318 | +While that may be necessary for the specific test, it's important to |
319 | +understand that this code changes global state and thus can adversely affect |
320 | +all of our other tests. In fact, this code caused intermittent and very |
321 | +difficult-to-debug failures that mucked up buildbot for many unrelated |
322 | +branches. |
323 | + |
324 | +The guideline then is this: if code changes global state (for example, by |
325 | +monkey-patching a module's globals) then the test **must** be sure to |
326 | +restore the previous state, either in a ``try``-``finally`` clause, or at |
327 | +the end of the doctest, or in the test's ``tearDown`` hook. |
328 | + |
329 | +Style to Avoid |
330 | +-------------- |
331 | + |
332 | +A very important consideration is that documentation tests are really |
333 | +**documentation** that happens to be testable. So, the writing style should |
334 | +be appropriate for documentation. It should be affirmative and descriptive. |
335 | +There shouldn't be any phrases like: |
336 | + |
337 | +* Test that... |
338 | +* Check that... |
339 | +* Verify that... |
340 | +* This test... |
341 | + |
342 | +While these constructs may help the reader understand what is happening, |
343 | +they only have indirect value as documentation. They can usually be |
344 | +replaced by simply stating what the result is. |
345 | + |
346 | +For example: |
347 | + |
348 | +.. code-block:: rst |
349 | + |
350 | + Test that the bar was added to the foo's related_bars: |
351 | + |
352 | + >>> bar in foo.related_bars |
353 | + True |
354 | + |
355 | +Can be replaced by: |
356 | + |
357 | +.. code-block:: rst |
358 | + |
359 | + After being linked, the bar is available in the foo's |
360 | + related_bars attribute: |
361 | + |
362 | + >>> bar in foo.related_bars |
363 | + True |
364 | + |
365 | +Also, use of "should" or "will" can usually be replaced by the present tense |
366 | +to make the style affirmative. |
367 | + |
368 | +For example: |
369 | + |
370 | +.. code-block:: rst |
371 | + |
372 | + The bar not_a_foo attribute should now be set: |
373 | + |
374 | + >>> bar.not_a_foo |
375 | + True |
376 | + |
377 | +Can be replaced by: |
378 | + |
379 | +.. code-block:: rst |
380 | + |
381 | + The bar not_a_foo attribute is set after this operation: |
382 | + |
383 | + >>> bar.not_a_foo |
384 | + True |
385 | + |
386 | +A good rule of thumb to know whether the narrative style works as |
387 | +documentation is to read the narrative as if the code examples were not |
388 | +there. If the text style makes sense, the style is probably good. |
389 | + |
390 | +Using Sample Data |
391 | +----------------- |
392 | + |
393 | +If possible, avoid using the existing sample data in tests, apart from some |
394 | +basic objects, like users. Sample data is good for demonstrating the UI, |
395 | +but it can make tests harder to understand, since it requires knowledge of |
396 | +the properties of the used sample data. Using sample data in tests also |
397 | +makes it harder to modify the data. |
398 | + |
399 | +If you do use sample data in the test, assert your expectations to avoid |
400 | +subtle errors if someone modifies it. For example: |
401 | + |
402 | +.. code-block:: rst |
403 | + |
404 | + Anonymous users can't see a private bug's description. |
405 | + |
406 | + >>> private_bug = getUtility(IBugSet).get(5) |
407 | + >>> private_bug.private |
408 | + True |
409 | + |
410 | + >>> login(ANONYMOUS) |
411 | + >>> private_bug.description |
412 | + Traceback (most recent call last): |
413 | + ... |
414 | + Unauthorized:... |
415 | + |
416 | +When using fake domains and **especially** fake email addresses, wherever |
417 | +possible use the ``example.{com,org,net}`` domains, e.g. |
418 | +``aperson@example.com``. These are guaranteed by Internet standard never to |
419 | +exist, so it can't be possible to accidentally spam them if something goes |
420 | +wrong on our end. |
421 | + |
422 | +Fixtures and Helpers |
423 | +-------------------- |
424 | + |
425 | +Sometimes a lot of code is needed to set up a test, or to extract the |
426 | +relevant information in the examples. It is usually a good idea to factor |
427 | +this code into functions that can be documented in the file itself (when the |
428 | +function will only be used in that file), or even better, moved into a test |
429 | +helper module from which you can import. |
430 | + |
431 | +These helpers currently live in ``lib/lp/testing``. New helpers should go |
432 | +there, unless they're very specific to a particular corner of the |
433 | +application; in such cases you can use something like ``lp.foo.testing``. |
434 | + |
435 | +Functional and Unit Tests |
436 | +========================= |
437 | + |
438 | +Complete test coverage without impairing documentation often requires |
439 | +dedicated functional or unit tests. In Launchpad, Python test cases are |
440 | +used for these types of tests. You may encounter legacy code that uses |
441 | +doctests for functional testing. If you do, please consider converting it |
442 | +to a Python test case. |
443 | + |
444 | +Functional tests are found in the ``tests`` subdirectory of each directory |
445 | +containing code under test. |
446 | + |
447 | +Python Test Cases |
448 | +----------------- |
449 | + |
450 | +Although Python test cases are not documentation they must still be |
451 | +human-readable. So: |
452 | + |
453 | +* Keep the test short and concise. |
454 | +* Stick to the "setup, exercise, assert" pattern, especially avoid |
455 | + "exercise, assert, exercise some more, assert". |
456 | +* Put into the docstring of each test case what is being tested. As a |
457 | + special case for test methods, a comment block at the beginning of the |
458 | + method is considered an acceptable substitute to a docstring. Please |
459 | + observe "Style to avoid", as explained above. |
460 | +* Organize related test cases in the same class. Explain test objectives in |
461 | + the class docstring. |
462 | +* When asserting for equality use the form ``assertEqual(expected_results, |
463 | + actual_results, "...")`` (the third argument is optional, for use if |
464 | + failure messages would otherwise be particularly unclear). |
465 | +* Make sure that each assert fails with an appropriate error message |
466 | + explaining what is expected. ``lp.testing.TestCase`` and |
467 | + ``TestCaseWithFactory`` are derived from ``testtools.TestCase`` and |
468 | + therefore produce good error messages. Only some cases may warrant an |
469 | + explicit error message. For example, this: |
470 | + |
471 | +.. code-block:: python |
472 | + |
473 | + self.assertTrue('aString' in result) |
474 | + |
475 | +could be replaced by: |
476 | + |
477 | +.. code-block:: python |
478 | + |
479 | + self.assertIn('aString', result) |
480 | + |
481 | +* Consider using testtools matchers where reasonable. These can often |
482 | + improve failure messages so that they show more information in one go, |
483 | + which can be useful when debugging mysterious failures. For example, |
484 | + instead of this: |
485 | + |
486 | +.. code-block:: python |
487 | + |
488 | + self.assertEqual('expected', obj.foo) |
489 | + self.assertEqual('more expected', obj.bar) |
490 | + |
491 | +prefer this: |
492 | + |
493 | +.. code-block:: python |
494 | + |
495 | + self.assertThat(obj, MatchesStructure.byEquality( |
496 | + foo='expected', |
497 | + bar='more expected')) |
498 | + |
499 | +In general, you should follow Launchpad coding conventions (see :doc:`Python |
500 | +style guide <python>`), however when naming test methods: |
501 | + |
502 | +* Use PEP 8 names, e.g. ``test_for_my_feature()`` |
503 | +* When testing a specific Launchpad method, a mix of PEP 8 and camel case is |
504 | + used, e.g. ``test_fooBarBaz()`` |
505 | +* When testing alternatives for a LP method, use this style: |
506 | + ``test_fooBarBaz_with_first_alternative()``, |
507 | + ``test_fooBarBaz_with_second_alternative()``, etc. |
508 | + |
509 | +How To Use the Correct Test URL |
510 | +=============================== |
511 | + |
512 | +When tests run, and need to connect to the application server instance under |
513 | +test, you need to ensure that a URL with the correct port for that test |
514 | +instance is used. Here's how to do that. |
515 | + |
516 | +The config instance has an API which allows the correct URL to be |
517 | +determined. The API is defined in ``LaunchpadConfig`` and as a convenience |
518 | +is available as a class method on ``BaseLayer``. |
519 | + |
520 | +.. code-block:: python |
521 | + |
522 | + def appserver_root_url(self, facet='mainsite', ensureSlash=False): |
523 | + """Return the correct app server root url for the given facet.""" |
524 | + |
525 | +Code snippets for a number of scenarios are as follows. |
526 | + |
527 | +**Doc Tests** |
528 | + |
529 | +.. code-block:: pycon |
530 | + |
531 | + >>> from lp.testing.layers import BaseLayer |
532 | + >>> root_url = BaseLayer.appserver_root_url() |
533 | + >>> browser.open(root_url) |
534 | + |
535 | +**Unit Tests** |
536 | + |
537 | +.. code-block:: python |
538 | + |
539 | + class TestOpenIDReplayAttack(TestCaseWithFactory): |
540 | + layer = AppServerLayer |
541 | + |
542 | + def test_replay_attacks_do_not_succeed(self): |
543 | + browser = Browser(mech_browser=MyMechanizeBrowser()) |
544 | + browser.open('%s/+login' % self.layer.appserver_root_url()) |
545 | diff --git a/doc/index.rst b/doc/index.rst |
546 | index b074f58..ff19541 100644 |
547 | --- a/doc/index.rst |
548 | +++ b/doc/index.rst |
549 | @@ -29,6 +29,7 @@ Guides |
550 | |
551 | guides/architecture |
552 | guides/python |
553 | + guides/tests |
554 | |
555 | Technical |
556 | ========= |
The link in the MP description is not correct - it should be https:/ /dev.launchpad. net/TestsStyleG uide
Other than that - LGTM