← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:tests-style-guide into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:tests-style-guide into launchpad:master with ~cjwatson/launchpad:dev-guides-formatting as a prerequisite.

Commit message:
Add tests style guide from dev.launchpad.net

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/411739

This was previously https://dev.launchpad.net/PythonStyleGuide.  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.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:tests-style-guide into launchpad:master.
diff --git a/doc/guides/python.rst b/doc/guides/python.rst
index c426bd6..be09dfe 100644
--- a/doc/guides/python.rst
+++ b/doc/guides/python.rst
@@ -34,7 +34,7 @@ Related Documents
 * `Exception guidelines <https://dev.launchpad.net/ExceptionGuidelines>`_
 * `Assertions <https://dev.launchpad.net/AssertionsInLaunchpad>`_
 * `Launchpad hacking FAQ <https://dev.launchpad.net/LaunchpadHackingFAQ>`_
-* `Tests style guide <https://dev.launchpad.net/TestsStyleGuide>`_
+* :doc:`Tests style guide <tests>`
 
 Whitespace and Wrapping
 =======================
diff --git a/doc/guides/tests.rst b/doc/guides/tests.rst
new file mode 100644
index 0000000..0615ae6
--- /dev/null
+++ b/doc/guides/tests.rst
@@ -0,0 +1,525 @@
+=================
+Tests Style Guide
+=================
+
+This page documents conventions for our Launchpad tests.  Reviewers make
+sure that code merged is documented and covered by tests.  Following the
+principles outlined in this document will minimize comments related to test
+style from reviewers.
+
+Reviewers will block merge of code that is under-documented or under-tested.
+We have two primary means of documentation:
+
+#. System documentation in ``doc`` directories such as ``lib/lp/<app>/doc``.
+#. Page tests in ``lib/lp/<app>/stories``.
+#. View tests in ``lib/lp/<app>/browser/tests``.
+
+While these two types of documentation use the doctest format, which means
+that they contain testable examples, they are documentation first.  So they
+are not the best place to test many corner cases or various similar
+possibilities.  This is best done in other unit tests or functional tests,
+which have ensuring complete test coverage as their main objective.
+
+Testable Documentation
+======================
+
+Testable documentation includes system documentation doctests and page
+tests.
+
+What do we mean by "testable documentation"?  One rule of thumb is the
+narrative should be similar to what you'd see in an API reference manual.
+Another trick to ensure the narrative is appropriately detailed is to
+mentally remove all of the code snippets and see if the narrative stands by
+itself.
+
+System Documentation
+--------------------
+
+These are doctests located under ``lib/lp/<app>/doc``.  They are used to
+document the APIs and other internal objects.  The documentation should
+explain to a developer how to use these objects and what purpose they serve.
+
+Each modification to ``lp.<app>.interfaces`` should be documented in one of
+these files.
+
+(Each file in that directory is automatically added to the test suite.  If
+you need to configure the test layer in which the test will be run or need
+to customize the test fixture, you can add special instructions for the file
+in the system documentation harness in ``lib/lp/<app>/tests/test_doc.py``.)
+
+Use Cases Documentation: Page Tests
+-----------------------------------
+
+We use page tests to document all the use cases that Launchpad satisfies.
+The narrative in these files should document the use case.  That is, they
+should explain what the user's objective is and how he accomplishes it.
+
+The examples in these files uses ``zope.testbrowser`` to show how the user
+would navigate the workflow relevant to the use case.
+
+So each addition to the UI should be covered by an appropriate section in a
+page test.
+
+The page tests do not need to document and demonstrate each and every
+possible way to navigate the workflow.  This can usually be done in a more
+direct manner by testing the view object directly. 
+
+Browser View Tests
+------------------
+
+View objects are usually documented that way along other system objects in
+files named ``*-pages.txt`` or in
+``lib/lp/<app>/browser/tests/*-views.txt``.
+
+The browser tests directory contains both doctest files for documenting the
+use of browser view classes and unit tests (e.g. ``test_*.py``) for
+performing unit tests, including covering corner cases.  Currently some
+apps, registry for example, have a large number of doctests in this location
+that are not strictly testable documentation.  Over time these
+non-documentation doctest files should be converted to unit tests. 
+
+**All new browser tests that are not testable documentation should be
+written as unit tests.**
+
+Common Conventions
+------------------
+
+The basic conventions for testable documentation are:
+
+* Example code is wrapped at 78 columns, follows the regular :doc:`Python
+  style guide <python>`, and is indented 4 spaces.
+* Narrative text may be wrapped at either 72 or 78 columns.
+* You can use regular Python comments for explanations related to the code
+  and not to the documentation.
+* Doctests use Restructured Text (or "ReST", see
+  http://docutils.sourceforge.net/docs/user/rst/quickref.html).
+
+  * We use ReST because it's what the Python community have standardized on
+    and because it makes setting up Sphinx to browse all the doctests
+    simpler.
+
+* The file should have a first-level title element.  An expansion of the
+  filename is usually a good start.  For example, the file
+  ``bugcomment.txt`` could have this title:
+
+.. code-block:: rst
+
+    Bug Comments
+    ============
+
+* Two blank lines are used to separate the start of a new section (a header).
+
+.. code-block:: rst
+
+    An Example
+    ----------
+
+    Launchpad tracks foo and bar elements using the IFooBarSet utility.
+
+        >>> from lp.foo.interfaces.bar import IBar, IFoo, IFooBarSet
+        >>> from lp.testing import verifyObject
+        >>> foobarset = getUtility(IFooBarSet)
+
+        >>> verifyObject(IFooBarSet, foobarset)
+        True
+
+    You use the getFoo() method to obtain an IFoo instance by id:
+
+        >>> foo = foobarset.getFoo('aFoo')
+        >>> verifyObject(IFoo, foo)
+        True
+
+    Similarly, you use the getBar() method to retrieve an IBar instance by
+    id:
+
+        >>> bar = foobarset.getBar('aBar')
+        >>> verifyObject(IBar, bar)
+        True
+
+Each individual test should be of the form:
+
+.. code-block:: pycon
+
+     >>> do_something()
+     expected output
+
+This means that something like this isn't considered a test, but test setup
+(since it doesn't produce any output):
+
+.. code-block:: pycon
+
+    >>> do_something()
+
+For the reason above, the assert statement shouldn't be used in doctests.
+
+Comparing Results
+-----------------
+
+When writing doctests, make sure that if the test fails, the failure message
+will be helpful to debug the problem.  Avoid constructs like:
+
+.. code-block:: pycon
+
+    >>> 'Test' in foo.getText()
+    True
+
+The failure message for this test will be:
+
+.. code-block:: pycon
+
+    - True
+    + False
+
+which isn't helpful at all in understanding what went wrong. This
+example is a lot more helpful when it fails:
+
+.. code-block:: pycon
+
+    >>> foo.getText()
+    '...Test...'
+
+For page tests where the page contains a lot of elements, you should zoom in
+to the relevant part.  You can use the ``find_main_content()``,
+``find_tags_by_class()``, ``find_tag_by_id()``, and ``find_portlet()``
+helper methods.  They return ``BeautifulSoup`` instances, which makes it
+easy to access specific elements in the tree.
+
+.. code-block:: rst
+
+    The new status is displayed in the portlet.
+
+        >>> details_portlet = find_portlet(browser.contents, 'Question details')
+        >>> print(details_portlet.find('b', text='Status:').next.strip())
+        Needs information
+
+There is also an ``extract_text()`` helper that only renders the HTML text:
+
+.. code-block:: pycon
+
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'branchtable')))
+    main         60 New           firefox
+    klingon      30 Experimental  gnome-terminal
+    junk.contrib 60 New 2005-10-31 12:03:57 ... weeks ago
+
+Read `Page tests <https://dev.launchpad.net/PageTests>`_ for other tips on
+writing page tests.
+
+When to print and when to return values
+---------------------------------------
+
+Doctests mimic the Python interactive interpreter, so generally it's
+preferred to simply return values and expect to see their string
+representation.  In a few cases though, it's better to ``print`` the results
+instead of just returning them.
+
+The two most common cases of this are ``None`` and strings.  The interactive
+interpreter suppresses ``None`` return values, so relying on these means the
+doctest makes less sense.  You could compare against ``None``, but the
+``True`` or ``False`` output isn't explicit, so it's almost always better to
+print values you expect to be ``None``.
+
+Instead of:
+
+.. code-block:: pycon
+
+    >>> should_be_none()
+    >>> do_something_else()
+
+Use:
+
+.. code-block:: pycon
+
+    >>> print(should_be_none())
+    None
+    >>> do_something_else()
+
+For a different reason, it's also usually better to print string results
+rather than just returning them.  Returning the string causes the quotes to
+be included in the output, while printing the string does not.  Those extra
+quotes are usually noise.
+
+Instead of:
+
+.. code-block:: pycon
+
+    >>> get_some_text()
+    'foo'
+    >>> get_some_string()
+    "Don't care"
+
+Use:
+
+.. code-block:: pycon
+
+    >>> print(get_some_text())
+    foo
+    >>> print(get_some_string())
+    Don't care
+
+Dictionaries and sets
+---------------------
+
+You can't just print the value of a dictionary or a set when that collection
+has more than one element in it, e.g.
+
+.. code-block:: pycon
+
+    >>> print(my_dict)
+    {'a': 1, 'b': 2}
+
+The reason is that Python does not guarantee the order of its elements in a
+dictionary or set, so the printed representation of a dictionary is
+indeterminate.  In page tests, there's a ``pretty()`` global which is
+basically exposing Python's pretty printer, and you can use it safely:
+
+.. code-block:: pycon
+
+    >>> pretty(my_dict)
+    {'a': 1, 'b': 2}
+
+Though it's a bit uglier, you can also print the sorted items of a
+dictionary:
+
+.. code-block:: pycon
+
+    >>> sorted(my_dict.items())
+    [('a', 1), ('b', 2)]
+
+Global State
+------------
+
+Be especially careful of test code that changes global state.  For example,
+we were recently bitten by code in a test that did this:
+
+.. code-block:: python
+
+    socket.setdefaulttimeout(1)
+
+While that may be necessary for the specific test, it's important to
+understand that this code changes global state and thus can adversely affect
+all of our other tests.  In fact, this code caused intermittent and very
+difficult-to-debug failures that mucked up buildbot for many unrelated
+branches.
+
+The guideline then is this: if code changes global state (for example, by
+monkey-patching a module's globals) then the test **must** be sure to
+restore the previous state, either in a ``try``-``finally`` clause, or at
+the end of the doctest, or in the test's ``tearDown`` hook.
+
+Style to Avoid
+--------------
+
+A very important consideration is that documentation tests are really
+**documentation** that happens to be testable.  So, the writing style should
+be appropriate for documentation.  It should be affirmative and descriptive.
+There shouldn't be any phrases like: 
+
+* Test that...
+* Check that...
+* Verify that...
+* This test...
+
+While these constructs may help the reader understand what is happening,
+they only have indirect value as documentation.  They can usually be
+replaced by simply stating what the result is.
+
+For example:
+
+.. code-block:: rst
+
+    Test that the bar was added to the foo's related_bars:
+
+        >>> bar in foo.related_bars
+        True
+
+Can be replaced by:
+
+.. code-block:: rst
+
+    After being linked, the bar is available in the foo's
+    related_bars attribute:
+
+        >>> bar in foo.related_bars
+        True
+
+Also, use of "should" or "will" can usually be replaced by the present tense
+to make the style affirmative.
+
+For example:
+
+.. code-block:: rst
+
+    The bar not_a_foo attribute should now be set:
+
+        >>> bar.not_a_foo
+        True
+
+Can be replaced by:
+
+.. code-block:: rst
+
+    The bar not_a_foo attribute is set after this operation:
+
+        >>> bar.not_a_foo
+        True
+
+A good rule of thumb to know whether the narrative style works as
+documentation is to read the narrative as if the code examples were not
+there.  If the text style makes sense, the style is probably good.
+
+Using Sample Data
+-----------------
+
+If possible, avoid using the existing sample data in tests, apart from some
+basic objects, like users.  Sample data is good for demonstrating the UI,
+but it can make tests harder to understand, since it requires knowledge of
+the properties of the used sample data.  Using sample data in tests also
+makes it harder to modify the data.
+
+If you do use sample data in the test, assert your expectations to avoid
+subtle errors if someone modifies it.  For example:
+
+.. code-block:: rst
+
+    Anonymous users can't see a private bug's description.
+
+        >>> private_bug = getUtility(IBugSet).get(5)
+        >>> private_bug.private
+        True
+
+        >>> login(ANONYMOUS)
+        >>> private_bug.description
+        Traceback (most recent call last):
+        ...
+        Unauthorized:...
+
+When using fake domains and **especially** fake email addresses, wherever
+possible use the ``example.{com,org,net}`` domains, e.g.
+``aperson@xxxxxxxxxxx``.  These are guaranteed by Internet standard never to
+exist, so it can't be possible to accidentally spam them if something goes
+wrong on our end.
+
+Fixtures and Helpers
+--------------------
+
+Sometimes a lot of code is needed to set up a test, or to extract the
+relevant information in the examples.  It is usually a good idea to factor
+this code into functions that can be documented in the file itself (when the
+function will only be used in that file), or even better, moved into a test
+helper module from which you can import.
+
+These helpers currently live in ``lib/lp/testing``.  New helpers should go
+there, unless they're very specific to a particular corner of the
+application; in such cases you can use something like ``lp.foo.testing``.
+
+Functional and Unit Tests
+=========================
+
+Complete test coverage without impairing documentation often requires
+dedicated functional or unit tests.  In Launchpad, Python test cases are
+used for these types of tests.  You may encounter legacy code that uses
+doctests for functional testing.  If you do, please consider converting it
+to a Python test case.
+
+Functional tests are found in the ``tests`` subdirectory of each directory
+containing code under test.
+
+Python Test Cases
+-----------------
+
+Although Python test cases are not documentation they must still be
+human-readable.  So:
+
+* Keep the test short and concise.
+* Stick to the "setup, exercise, assert" pattern, especially avoid
+  "exercise, assert, exercise some more, assert".
+* Put into the docstring of each test case what is being tested.  As a
+  special case for test methods, a comment block at the beginning of the
+  method is considered an acceptable substitute to a docstring.  Please
+  observe "Style to avoid", as explained above.
+* Organize related test cases in the same class.  Explain test objectives in
+  the class docstring.
+* When asserting for equality use the form ``assertEqual(expected_results,
+  actual_results, "...")`` (the third argument is optional, for use if
+  failure messages would otherwise be particularly unclear).
+* Make sure that each assert fails with an appropriate error message
+  explaining what is expected.  ``lp.testing.TestCase`` and
+  ``TestCaseWithFactory`` are derived from ``testtools.TestCase`` and
+  therefore produce good error messages.  Only some cases may warrant an
+  explicit error message.  For example, this:
+
+.. code-block:: python
+
+    self.assertTrue('aString' in result)
+
+could be replaced by:
+
+.. code-block:: python
+
+    self.assertIn('aString', result)
+
+* Consider using testtools matchers where reasonable.  These can often
+  improve failure messages so that they show more information in one go,
+  which can be useful when debugging mysterious failures.  For example,
+  instead of this:
+
+.. code-block:: python
+
+    self.assertEqual('expected', obj.foo)
+    self.assertEqual('more expected', obj.bar)
+
+prefer this:
+
+.. code-block:: python
+
+    self.assertThat(obj, MatchesStructure.byEquality(
+        foo='expected',
+        bar='more expected'))
+
+In general, you should follow Launchpad coding conventions (see :doc:`Python
+style guide <python>`), however when naming test methods:
+
+* Use PEP 8 names, e.g. ``test_for_my_feature()``
+* When testing a specific Launchpad method, a mix of PEP 8 and camel case is
+  used, e.g. ``test_fooBarBaz()``
+* When testing alternatives for a LP method, use this style:
+  ``test_fooBarBaz_with_first_alternative()``,
+  ``test_fooBarBaz_with_second_alternative()``, etc.
+
+How To Use the Correct Test URL
+===============================
+
+When tests run, and need to connect to the application server instance under
+test, you need to ensure that a URL with the correct port for that test
+instance is used.  Here's how to do that.
+
+The config instance has an API which allows the correct URL to be
+determined.  The API is defined in ``LaunchpadConfig`` and as a convenience
+is available as a class method on ``BaseLayer``.
+
+.. code-block:: python
+
+    def appserver_root_url(self, facet='mainsite', ensureSlash=False):
+        """Return the correct app server root url for the given facet."""
+
+Code snippets for a number of scenarios are as follows.
+
+**Doc Tests**
+
+.. code-block:: pycon
+
+    >>> from lp.testing.layers import BaseLayer
+    >>> root_url = BaseLayer.appserver_root_url()
+    >>> browser.open(root_url)
+
+**Unit Tests**
+
+.. code-block:: python
+
+    class TestOpenIDReplayAttack(TestCaseWithFactory):
+        layer = AppServerLayer
+
+        def test_replay_attacks_do_not_succeed(self):
+            browser = Browser(mech_browser=MyMechanizeBrowser())
+            browser.open('%s/+login' % self.layer.appserver_root_url())
diff --git a/doc/index.rst b/doc/index.rst
index b074f58..ff19541 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -29,6 +29,7 @@ Guides
 
    guides/architecture
    guides/python
+   guides/tests
 
 Technical
 =========