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
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
Behaviour Driven Development
- describes business value
- helps QA
detailed description to achieve a (sub)goal
- crud content in webapp
- move content forward in workflow
- ...
- write tests in a way the technical writer can understand
- capture screenshots to illustrate the documentation
- annotate the screnshots with information
- output the documentation
There is tons of tools to choose from.
- 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
- 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
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
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 <--------+ | +----^----+ | | | | | | | +---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
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
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
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
@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
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
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
@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
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
@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
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
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
quick demo ?
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%
$ rst2pdf -o devpi.pdf use_devpi.rst
$ rst2html devpi.pdf use_devpi.html
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
Thanks
Space | Forward |
---|---|
Right, Down, Page Down | Next slide |
Left, Up, Page Up | Previous slide |
P | Open presenter console |
H | Toggle this help |