The Biggest Mistakes Django Developers Make When Using Lettuce
Join the DZone community and get the full member experience.
Join For Freethis 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?
-
step.behave_as implementation has issues .
-
if you have to bypass parameters to the target steps, you will need to concatenate or interpolate strings, which will easily become a mess.
-
if the string you pass as a parameter has typos, it’s a pain to debug.
-
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.
-
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.
-
hardcoded strings would require manual updates when any related step-definitions has its regex changed.
-
step definitions that are multiple-lines long now just bypass the parameters into single-line function calls.
-
when the hardcoded string has typos, no syntax error will occur yet the test will fail with a misleading error message.
Published at DZone with permission of Gabriel Falcão. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments