Test Driven documentation

Situation

We have a complex web app.

  • user base is evolving
  • development team rotates
  • business specifics are non trivial

We need a decent documentation

  • overall goal of the project
  • step by step instructions

Story

Stories describe role, expectations and benefits.

As a Developer I want to access the in house package repository So that I can install the latest and greatest software

  • Small
  • Unambiguous
  • Long lasting

You can use the INVEST mnemonic

  • Independent
  • Negotiable
  • Valuable
  • Estimatable
  • Small
  • Testable

Acceptance tests

Behaviour Driven Development

  • describes business value
  • helps QA

Documentation oriented tests

detailed description to achieve a (sub)goal

  • crud content in webapp
  • move content forward in workflow
  • ...

What we want

  • write tests in a way the technical writer can understand
  • capture screenshots to illustrate the documentation
  • annotate the screnshots with information
  • output the documentation

The Python cheeseshop is full of gems

There is tons of tools to choose from.

  • This is an opinionated solution

Today's specials

  • a web app: devpi
  • write functional tests : gherkin
  • execute functional tests : behave
  • make sure output is ok: hamcrest
  • drive output, get screenshots: selenium
  • annotate on screen: ninepatch
  • generate documentation: rst2pdf

Devpi

  • proxy to pypi cheeseshop
  • repos of private packages
  • each developper can have its own private repos
  • repos hierarchy
  • ldap enabled
  • builds the API docs

For the sake of demonstration, I'll use devpi

  • allows to work with virtualenvs offline
  • store and distribute packages internally
  • publish API documentation
  • show app working

Gherkin

Feature: Devpi server list our packages

  As a    Developer
  I want  to access the in house package repository
  So that I can install the latest and greatest software

  Scenario Outline: see my index
    Given I access the main devpi page as "nlaurance"
    When I click My personal repository
    Then I can see my "dev" index

Usually given, when ,then

  • setup prerequisites
  • some action
  • observe result (assert_that is hamcrest)

behave

Behave implements the features with steps

@given(u'I access the main devpi page as "{user}"')
def step_impl(context, user):
    context.browser.get('http://127.0.0.1:3141')
    context.user = user

@when(u'I click My personal repository')
def step_impl(context):
    index = 'dev'
    full_index = '/'.join((context.user, index))
    link_to_repos = context.browser.find_element_by_xpath("//a[text()='{0}']".format(full_index))
    link_to_repos.click()

@then(u'I can see my "{index}" index')
def step_impl(context, index):
    full_index = '/'.join((context.user, index))
    assert_that(context.browser.page_source, string_contains_in_order(full_index))

context object

"""
             +---------+
    +--------> context <--------+
    |        +----^----+        |
    |             |             |
    |             |             |
+---v----+    +---v----+    +---v----+
| Step 1 |    | Step 2 |    | Step 3 |
+--------+    +--------+    +--------+

"""
from selenium import webdriver

def before_all(context):
    context.browser = webdriver.Chrome()

def after_all(context):
    context.browser.quit()

Context is shared between steps

A good place to hold a screen capture pass it along and enrich it

ascii flow is nice for simple diagram cut/paste in docstrings (demo?)

simple decorators

Run the tests

command

behave features/devpi_simple.feature
Feature: Devpi server list our packages # features/devpi_simple.feature:1
  As a    Developer
  I want  to access the in house package repository
  So that I can install the latest and greatest software
  Scenario: see my index                              # features/devpi_simple.feature:7
    Given I access the main devpi page as "nlaurance" # features/steps/devpi.py:14 1.244s
    Then I can see my "dev" index                     # features/steps/decorators.py:51 0.047s

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
2 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m1.291s

let's make a quick digression about decorators

Decorator digression

a simple function

def base_function(number):
    """ --base func-- """
    return number * 2

print(base_function(5))
# 10
print(base_function.__doc__)
#  --base func--
print(base_function.__name__)
# base_function

Decorator digression

a function of function

def print_args(func):
    def replacement(number):
        """ -- replacement -- """
        print("number is {}".format(number))
        return func(number)
    return replacement

whats_this = print_args(base_function)

print(whats_this(10))
# number is 10
# 20
print(whats_this.__doc__)
#  -- replacement --
print(whats_this.__name__)
# replacement

Decorator digression

a simpler notation

@print_args
def base_function(number):
    """ -- base function -- """
    return number * 2

print(base_function(5))
# number is 5
# 10
print(base_function.__doc__)
#  -- replacement --
print(base_function.__name__)
# replacement

Decorator digression

polishing details

from functools import wraps

def print_args(func):
    @wraps(func)
    def replacement(number):
        """ -- replacement -- """
        print("number is {}".format(number))
        return func(number)
    return replacement

@print_args
def base_function(number):
    """ -- base function -- """
    return number * 2

print(base_function(5))
# number is 5
# 10
print(base_function.__doc__)
#  -- base function --
print(base_function.__name__)
# base_function

The feature description

Feature: Devpi server list our personal packages

  As a    Developer
  I want  to access the in house package repository
  So that I can install the latest and greatest software

  Scenario: see my personal index
    Given I access the main devpi page as "nlaurance"
    When I click My personal repository
    Then I have permission to upload packages
    And I want to "outline" this element
    And I annotate this in the "se" with
       """
       Make sure the upload permission
       is set for you
       """
    Then I see my package list
    And I want to "outline" this element
    And I annotate this in the "ne" with
       """
       Link to detailed information
       and direct download of the package
       """
    And I want a screenshot as "annotated.png"

to outline to annotate

steps to manipulate a screenshot

Outline context

Step

@then(u'I see my package list')
@outline_elements_context
def step_impl(context):
    package_list = context.browser.find_element_by_class_name('packages')
    packages = package_list.find_elements_by_xpath("//td/a")
    assert_that(len(packages), greater_than_or_equal_to(1))
    return [dom_element_size(packages[0]),
            dom_element_size(packages[1])]

steps returns list of coordinates of interest in the screen

Outline context

outline decorator

def outline_elements_context(step):
    """ context.outline_elements is a list of
    x, y, width, height returned by dom_element_size
    keeping tracks of the parts of interest in the UI
    """

    @wraps(step)
    def wrapper(context, *args, **kwargs):
        outline_elements = getattr(context, 'outline_elements', [])
        new_elements = step(context, *args, **kwargs)
        outline_elements.extend(new_elements)
        context.outline_elements = outline_elements
        return new_elements

    return wrapper

Screenshot context

Step

@then(u'I want to "{hilight}" this element')
@then(u'I want to "{hilight}" these elements')
@screenshot_context
def step_impl(context, hilight):
    outline_elements = getattr(context, 'outline_elements', [])
    if hilight == "outline":
        commented = paste_outline(context.current_screenshot,
                                  outline_elements)
    return commented

screenshot context: step returns an image

Screenshot context

Decorator

def screenshot_context(step):
    """ context.current_screenshot keep tracks of the screenshot
    between steps
    """

    @wraps(step)
    def wrapper(context, *args, **kwargs):
        screenshot = getattr(context, 'current_screenshot',
                             Image.open(StringIO(context.browser.get_screenshot_as_png())))
        context.current_screenshot = screenshot

        updated_screenshot = step(context, *args, **kwargs)
        context.current_screenshot = updated_screenshot
        return updated_screenshot

    return wrapper

This is when the background screenshot is taken

Screenshot context

Enrich the screenshot

def paste_outline(screenshot, coords):
    """ center an outline image around all the given elements coordinates

    :param screenshot: current screenshot, PIL Image or StringIO
    :param coords: list of coordinates, tuples of x, y, width, height
    """
    for coord in coords:
        x, y, width, height = coord

        ninepatch = Ninepatch('./bubbles/neon.9.png')
        outline_img = ninepatch.render_to_fit((width, height))
        # fully centered
        outline_w, outline_h = outline_img.size
        paste_x = int(x - (outline_w - width) / 2)
        paste_y = int(y - (outline_h - height) / 2)

        screenshot.paste(outline_img, (paste_x, paste_y), outline_img)
    return screenshot

let's make a quick digression about ninepatch

Ninepatch digression

A technique from Android

Ninepatch digression

Set zone for content

Ninepatch digression

Nice but corners are a mess

Ninepatch digression

Set repeatable segments

quick demo ?

Write the doc

Devpi start screen
==================

some verbose explanation about what this does

.. image:: images/index.png
   :width: 80%

.. raw:: pdf

   PageBreak

Devpi personnal repository
==========================

.. image:: images/annotated.png
   :width: 80%

Build the doc

as PDF

$ rst2pdf -o devpi.pdf use_devpi.rst

as HTML

$ rst2html  devpi.pdf use_devpi.html

That's it

following the pattern you can create

  • easy to follow step by step FAQs answers
  • always up-to-date customized instructions (1 engine, n designs)

Writing good stories is another ... story

Questions ?

Thanks

SpaceForward
Right, Down, Page DownNext slide
Left, Up, Page UpPrevious slide
POpen presenter console
HToggle this help