Defining Steps

aloe.step(sentence=None)

Decorates a function, so that it will become a new step definition.

You give step sentence either (by priority):

  • with step function argument;
  • with function doc; or
  • with the function name exploded by underscores.

Parameters can be passed to steps using regular expressions. Parameters are passed in the order they are captured. Be aware that captured values are strings.

The first parameter passed into the decorated function is the Step object built for this step.

Examples:

@step("I go to the shops")
def _i_go_to_the_shops_step(self):
    '''Implements I go to the shops'''

    ...

@step
def _i_go_to_the_shops_step(self):
    '''I go to the shops'''

    ...

@step(r"I buy (\d+) oranges")
def _purchase_oranges_step(self, num_oranges):
    '''Buy a certain number of oranges'''

    num_oranges = int(num_oranges)

    ...

Steps can be passed a table of data.

Given the following users are registered:
    | Username | Real name |
    | danni    | Danni     |
    | alexey   | Alexey    |

This is exposed in the step as Step.table and Step.hashes.

@step(r'Given the following users? (?:is|are) registered:')
def _register_users(self):
    '''Register the given users'''

    for user in guess_types(self.hashes):
        register(username=user['Username'],
                 realname=user['Real name'])

Steps can be passed a multi-line “Python string”.

Then I see a warning dialog:
    """
    Changes could not be saved.

    [Try Again]
    """

This is exposed in the step as Step.multiline.

The registered function will have an unregister() method that removes all the step definitions that are associated with it.

Common regular expressions for capturing data

String

Given I logged in as "alexey"
@step(r'I logged in as "([^"]*)"')

Number

Then the price should be $12.99
@step(r'The price should be \$(\d+(?:\.\d+)?)')

Path/URI/etc.

Given I visit /user/alexey/profile
@step(r'I visit ([^\s]+)')

Step loading

Steps can and should be defined in separate modules to the main application code. Aloe searches for modules to load steps from inside the features directories.

Steps can be placed in separate files, for example, features/steps/browser.py and features/steps/data.py, but all those files must be importable, so this requires creating a (possibly empty) features/steps/__init__.py alongside.

Additional 3rd-party steps (such as aloe_django) can be imported in from your __init__.py.

An imported step can be overridden by using unregister() on the function registered as a step. It can be then reused by defining a new step with the same or different sentence.

Tools for step writing

Useful tools for writing Aloe steps.

See also aloe.world.

aloe.tools.guess_types(data)

Converts a record or list of records from strings contained in outlines, table or hashes into a version with the types guessed.

Parameters:data – a Scenario.outlines, Step.table, Step.hashes or any other list, list of lists or list of dicts.

Will guess the following (in priority order):

  • bool (true/false)
  • None (null)
  • int
  • date in ISO format (yyyy-mm-dd)
  • str

The function operates recursively, so you should be able to pass nearly anything to it. At the very least basic types plus dict and iterables.

aloe.tools.hook_not_reentrant(func)

Decorate a hook as unable to be reentered while it is already in the stack.

Any further attempts to enter the hook before exiting will be replaced by a no-op.

This is generally useful for step hooks where a step might call Step.behave_as() and trigger a second level of step hooks i.e. when displaying information about the running test.

Writing good BDD steps

It’s very easy with BDD testing to accidentally reinvent Python testing using a pseudo-language. Doing so removes much of the point of using BDD testing in the first place, so here is some advice to help write better BDD steps.

  1. Avoid implementation details

    If you find yourself specifying implementation details of your application that aren’t important to your behaviors, abstract them into another step.

    Implementation:

    When I fill in username with "danni"
    And I fill in password with "secret"
    And I press "Log on"
    And I wait for AJAX to finish
    

    Behavioral:

    When I log on as "danni" with password "secret"
    

    You can use Step.behave_as() to write a step that chains up several smaller steps.

    Implementation:

    Given the following flags are set:
        | flags                      |
        | user_registration_disabled |
        | user_export_disabled       |
    

    Behavioral:

    Given user registration is disabled
    And user export is disabled
    

    Remember you can generate related steps using a loop.

    for description, flag in ( ... ):
        @step(description + ' is enabled')
        def _enable_flag(self):
    
            set_flag(flag, enabled=True)
    
        @step(description + ' is disabled')
        def _disable_flag(self):
    
            set_flag(flag, enabled=False)
    

    Furthermore, steps that are needed by all features can be moved to a each_example() callback.

    If you want to write reusable steps, you can sometimes mix behavior and declaration.

    Then I should see results:
        | Business Name (primaryText) | Blurb (secondaryText) |
        | Pet Supplies.com            | An online store for…  |
    
  2. Avoid conjunctions in steps

    If you’re writing a step that contains an and or other conjunction consider breaking your step into two.

    Bad:

    When I log out and log back in as danni
    

    Good:

    When I log out
    And I log in as danni
    

    You can pass state between steps using world.

  3. Support natural language

    It’s easier to write tests if the language they support is natural, including things such as plurals.

    Unnatural:

    Given there are 1 users in the database
    

    Natural:

    Given there is 1 user in the database
    

    This can be done with regular expressions.

    @step('There (?:is|are) (\d+) users? in the database')