Merge ~cjwatson/launchpad:tests-style-guide into launchpad: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)
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://dev.launchpad.net/TestsStyleGuide. I've kept the material essentially unchanged, except for converting to reStructuredText formatting; I think it probably over-emphasizes doctests, but we can clean that up later.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

The link in the MP description is not correct - it should be https://dev.launchpad.net/TestsStyleGuide

Other than that - LGTM

review: Approve
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
1diff --git a/doc/guides/python.rst b/doc/guides/python.rst
2index 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 =======================
14diff --git a/doc/guides/tests.rst b/doc/guides/tests.rst
15new file mode 100644
16index 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())
545diff --git a/doc/index.rst b/doc/index.rst
546index 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 =========

Subscribers

People subscribed via source and target branches

to status/vote changes: