Search This Blog

10 May 2015

TDD: Test Driven Documentation

What's Test Driven Documentation ?

Well the idea is pretty simple, while you write specification in Gherkin you describe the way you want the application to behave.
Writing the documentation is explaining how the application behaves so as to learn how to use it.
What if from good requirements we could infer part of the documentation ?

If we run these tests with behave and run a selenium driver we can leverage to take a few screenshots and add information about what's happening. In short we want to go from

Feature: Devpi server list our personal packages

  As a    Developer
  I want  to access the in house package repository
  So that I can develop 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 a "overlined" screenshot saved as "index.png"
    And I document in the "se" as "bubble_list.png" with
       """
       Make sure the upload permission
       is set for you
       """


to


let's have a peek at what's in devpi.py, inside the steps directory


def dom_element_size(dom_element):
    x, y = dom_element.location['x'], dom_element.location['y']
    width, height = dom_element.size['width'], dom_element.size['height']
    return x, y, width, height


@then(u'I have permission to upload packages')
def step_impl(context):
    permissions = context.browser.find_element_by_class_name('permissions')
    assert_that(permissions.text, contains_string(context.user))

    my_permission = permissions.find_element_by_xpath("descendant::span[contains(text(), '{0}')]".format(context.devpi_user))

    outline_elements = getattr(context, 'outline_elements', [])
    outline_elements.append(dom_element_size(my_permission))
    context.outline_elements = outline_elements

at this stage there is a hamcrest assertion, to make sure we comply to the requirement. Then we keep track of the desired element(s) position and size.

@then(u'I document in the "{hotspot}" as "{filename}" with')
def step_impl(context, hotspot, filename):
    outline_elements = getattr(context, 'outline_elements', [])

    screenshot = getattr(context, 'current_screenshot',
                         StringIO(context.browser.get_screenshot_as_png()))
    commented = paste_bubble(screenshot, outline_elements, hotspot, context.text, filename)
    context.current_screenshot = commented



We can take a screenshot at any time, to allow chaining we pass the current screenshot to the context.

so what is paste_bubble doing ?

it uses the ninepatch library to format image ressources to our need. In fact a fork of it to simplify the code.

def paste_bubble(screenshot, coords, hotspot, text, save_as='screenshot.png'):
    image = screenshot if isinstance(screenshot, Image.Image) else Image.open(screenshot)
    for coord in coords:
        x, y, width, height = coord

    # se
    paste_x = int(x + width)
    paste_y = int(y + height)

    bubble_img = bubble(text, hotspot)
    image.paste(bubble_img, (paste_x, paste_y), bubble_img)
    image.save(save_as)
    return image

Yes, this is still at the embryo stage, and the hotspot (position) is not really implemented yet.

def bubble(text, hotspot="se"):
    ninepatch = Ninepatch('./bubbles/{0}.9.png'.format(hotspot))
    text_as_image = text2img(text)
    scaled_bubble = ninepatch.render_with_content(text_as_image)
    if DEBUG:
        scaled_bubble.save('bubble.png')
    return scaled_bubble

Now some play with inkscape and nine patch editor, to create some outlines. In here we have a se.9.png file in the bubbles subdirectory.

This is a work in progress, but that first result is rewarding enough.
I'll see if this deserves a packaging of its own.