Over a million developers have joined DZone.

The Biggest Mistakes Django Developers Make When Using Lettuce

· Web Dev Zone

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

This post is the first in a series of posts about best practices when using Lettuce, a testing framework for Django.

When I first released Lettuce, a framework for writting automated tests in Django with user stories, I had no idea that it would have become so widely used. It’s been truly amazing to have seen it expand from Brazil to the United States to China and many other countries. It’s even been translated into 15 languages.

However, over the last 6 months, I’ve observed a common usage that, for the reasons below, developers should avoid.

Steps from Step Definition

Like Cucumber, Lettuce supports calling other steps from a step definition. This can be a very handy functionality, but can easily become a source of code that is hard to maintain.

So why is this functionality available? Although Lettuce is a testing tool, step.behave_as was a patch) that was incorporated in the codebase without complete test coverage. step.behave_as causes a step to call many others by parsing the text and calling them synchronously and sequentially.

Some people like to use this functionality in order to make their scenario look leaner, which is fine. The actual problem is that this workflow is sub-optimal, so I would advise using this functionality with caution.

An example of step.behave_as usage (please avoid doing the same) As an example, let’s consider the following feature and its respective step definitions:

Feature: exemplify why step.behave_as can be a not-so-good option

  Scenario: Login should work
    Given I am at the login page
    When I type the username "root"
    And I type the password "123"
    And I try to perform the login
    Then it works and I am redirected to the admin page

  Scenario: Get in the user management admin page
    Given I log in as "admin" with password "123"
    When I click to manage users
    Then I am pointed out to the user management page

defined as:

from lettuce import *
from lettuce.django import django_url
from splinter.browser import Browser

PAGES = {
    "the login page": "/login",
    "the user management page": "/manage/users"
}

def page_name_is_valid(name):
    assert PAGES.has_key(name), \
        'the page "%s" is not mapped in the PAGES dictionary, ' \
        'check if you misspelled it or add into it' % name
    return True

@before.each_scenario
def prepare_browser(scenario):
    world.browser = Browser()

@step(ur'I am at (.*)')
def i_am_at_some_url(step, name):
    assert page_name_is_valid(name)

    full_url = django_url(PAGES[name])
    world.browser.visit(full_url)

@step(ur'I type the (\S+) "(.*)"')
def i_type_some_value_into_a_field(step, field, value):
    browser.fill(field, value)

@step(ur'I try to perform the login')
def try_to_perform_login(step, field, value):
    browser.find_by_css("button#submit-login").click()

@step(ur'it works and I am redirected to (.*)')
def it_works_and_im_redirected_to(step, name):
    assert page_name_is_valid(name)

    current_url = world.browser.url
    full_url = django_url(PAGES[name])

    assert current_url == full_url, \
        'the current url is "%s" but should be "%s"' % (current_url, full_url)

@step(ur'I log in as "(\w+)" with password "(.*)"')
def i_log_in_as(step, username, password):
    step.behave_as(ur'''
        Given I am at the login page
        When I type the username "%s"
        And I type the password "%s"
        And I try to perform the login
        Then it works and I am redirected to the admin page
    ''' % (username, password))

@step(ur'I click to manage users')
def try_to_perform_login(step, field, value):
    browser.find_by_css("button#manage-users").click()

@step(ur'I am pointed out to (.*)')
def im_pointed_out_to(step, name):
    step.then("it works and I am redirected to %s" % name)

So… it looks kinda nice, why is it bad?

  1. step.behave_as implementation has issues.

  2. if you have to bypass parameters to the target steps, you will need to concatenate or interpolate strings, which will easily become a mess.

  3. if the string you pass as a parameter has typos, it’s a pain to debug.

  4. internally in Lettuce’s codebase, every single step is built from an object which is bound to the parent scenario, and metadata such as where it is defined. The current step.behave_as implementation doesn’t remount those aspects properly, leading to craziness when debugging.

  5. once you hardcode strings in your step definitions, your test’s codebase will get hard to scale to more developers, and thus, hard to maintain.XXX


This is how Lettuce works if you are not using step.behave_as:


Please note the two aditional steps when you use it
:

The solution: refactor generic step definitions into @world.absorb methods

Lettuce provides @world.absorb, a handy decorator, for storing useful and generic functions in a global test scope. The @world.absorb decorator literally absorbs the decorated function into the world helper and can be used right away in any other python files.

This decorator was created precisely for leveraging the refactoring of step definitions and terrain helpers by not requiring the developer to make too many imports from different paths, as well as to avoid making relative imports. Let’s see how the first example would look like when using @world.absorb.

from lettuce import *
from lettuce.django import django_url
from splinter.browser import Browser

PAGES = {
    "the login page": "/login",
    "the user management page": "/manage/users"
}

def page_name_is_valid(name):
    assert PAGES.has_key(name), \
        'the page "%s" is not mapped in the PAGES dictionary, ' \
        'check if you misspelled it or add into it' % name
    return True

@world.absorb
def go_to_page(name):
    assert page_name_is_valid(name)

    full_url = django_url(PAGES[name])
    world.browser.visit(full_url)

@world.absorb
def assert_current_page_is(name):
    assert page_name_is_valid(name)

    current_url = world.browser.url
    full_url = django_url(PAGES[name])

    assert current_url == full_url, \
        'the current url is "%s" but should be "%s"' % (current_url, full_url)

@before.each_scenario
def prepare_browser(scenario):
    world.browser = Browser()

@step(ur'I am at (.*)')
def i_am_at_some_url(step, name):
    world.go_to_page(name)

@step(ur'I type the (\S+) "(.*)"')
def i_type_some_value_into_a_field(step, field, value):
    browser.fill(field, value)

@step(ur'I try to perform the login')
def try_to_perform_login(step, field, value):
    browser.find_by_css("button#submit-login").click()

@step(ur'it works and I am redirected to (.*)')
def it_works_and_im_redirected_to(step, the_expected_page):
    world.assert_current_page_is(the_expected_page)

@step(ur'I log in as "(\w+)" with password "(.*)"')
def i_log_in_as(step, username, password):
    world.go_to_page("the login page")
    browser.fill("username", username)
    browser.fill("password", password)
    browser.find_by_css("button#submit-login").click()
    world.assert_current_page_is("the admin page")

@step(ur'I click to manage users')
def try_to_perform_login(step, field, value):
    browser.find_by_css("button#manage-users").click()

@step(ur'I am pointed out to (.*)')
def im_pointed_out_to(step, expected_page):
    world.assert_current_page_is(the_expected_page)

The step definition def i_log_in_as now calls helpers that are available in the world helper.

Conclusion

You can easily notice that in the example above, **@world.absorb** allows for better maintainability and cleaner step definitions.

  1. Hardcoded strings would require manual updates when any related step-definitions has its regex changed.

  2. Step definitions that are multiple-lines long now just bypass the parameters into single-line function calls.

  3. When the hardcoded string has typos, no syntax error will occur yet the test will fail with a misleading error message.

 

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:

Published at DZone with permission of Gabriel Falcão. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}