API

class morelia.File(path)

Bases: Source

Marks string as a file path.

class morelia.Url(url)

Bases: Source

Marks string as an url endpoint.

morelia.run(filename, suite, as_str=None, scenario='.*', verbose=False, show_all_missing=True, **kwargs)

Parse file and run tests on given suite.

Parameters:
  • filename (str) – file name

  • suite (unittest.TestCase) – TestCase instance

  • as_str (string) – None to use file or a string containing the feature to parse

  • scenario (string) – a regex pattern to match the scenario to run

  • verbose (boolean) – be verbose

  • show_all_missing (boolean) – show all missing steps

morelia.verify(script, suite, scenario: str = '.*', config: str = 'default') None

Verifies script with steps from suite.

Parameters:
  • script – feature script

  • suite – object with steps defined

  • scenario (str) – regex pattern for selecting single scenarios

  • config (str) – section from configuration to apply

Script can be passed directly to verify method as first argument.

>>> from morelia import verify
>>> verify(
...    """
...    Feature: Addition
...    Scenario: Add two numbers
...      Given I have entered 50 into the calculator
...      And I have entered 70 into the calculator
...      When I press add
...      Then the result should be 120 on the screen
...    """,
...    test_case_with_steps,
)

When given path to file with script morelia will read that file and verify:

>>> verify('calculator.feature', test_case_with_steps)

Similary url pointing to script can be given:

>>> verify('http://example.com/calculator', test_case_with_steps)

Two last invocations will work only for single line strings. If it starts with “http[s]://” it is considered an url. If it ends with “.feature” it is considered a file.

To explicity mark parameter’s type it can be wrapped in helper classes:

  • File()

  • Url()

  • Text()

    >>> from morelia import verify, File, Text, Url
    >>> verify(File('calculator.txt'), test_case_with_steps)
    >>> verify(Url('http://example.com/calculator.feature'), test_case_with_steps)
    >>> verify(
    ...    Text("""
    ...    Feature: Addition
    ...    Scenario: Add two numbers
    ...      Given I have entered 50 into the calculator
    ...      And I have entered 70 into the calculator
    ...      When I press add
    ...      Then the result should be 120 on the screen
    ...    """),
    ...    test_case_with_steps,
    )
    

Steps

Matching steps

When Morelia executes steps described in feature files it looks inside passed unittest.TestCase object and search for methods which name starts with step_. Then it selects correct method using:

If you look in example from Quick usage guide:

# test_acceptance.py

from pathlib import Path
import unittest

from morelia import verify


class CalculatorTestCase(unittest.TestCase):

    def test_addition(self):
        ''' Addition feature '''
        filename = Path(__file__) / "calculator.feature"
        verify(filename, self)

    def step_I_have_powered_calculator_on(self):
        r'I have powered calculator on'
        self.stack = []

    def step_I_enter_a_number_into_the_calculator(self, number):
        r'I enter "(\d+)" into the calculator'  # match by regexp
        self.stack.append(int(number))

    def step_I_press_add(self):  #  matched by method name
        self.result = sum(self.stack)

    def step_the_result_should_be_on_the_screen(self, number):
        r'the result should be "{number}" on the screen'  # match by format-like string
        self.assertEqual(int(number), self.result)

You’ll see three types of matching.

Regular expressions

Method step_I_enter_number_into_the_calculator from example is matched by regular expression as it’s docstring

r'I enter "(\d+)" into the calculator'

matches steps:

When I enter "50" into the calculator
And I enter "70" into the calculator

Regular expressions, such as (\d+), are expanded into positional step arguments, such as number in above example. If you would use named groups like (?P<number>\d+) then capttured expressions from steps will be put as given keyword argument to method.

Remember to use tight expressions, such as (\d+), not expressions like (\d*) or (.*), to validate your input.

Format-like strings

Method step_the_result_should_be_on_the_screen from example is matched by format-like strings as it’s docstring

r'the result should be "{number}" on the screen'

matches step:

Then the result should be "120" on the screen

Method names

Method step_I_press_add from example is matched by method name which matches step:

And I press add

Choosing which matchers to use

By default morelia search for methods using in order:

  • format-like strings matcher (“parse” matcher)

  • regex matcher (“regex” matcher)

  • method names matcher (“method” matcher)

You can override it in your pyproject.toml file. E.g.:

[tool.morelia.default]
matchers=["regex", "parse"]

Tables

If you use Scenarios with tables and <angles> around the payload variable names:

Scenario: orders above $100.00 to the continental US get free ground shipping
  When we send an order totaling $<total>, with a 12345 SKU, to our warehouse
   And the order will ship to <destination>
  Then the ground shipping cost is $<cost>
   And <rapid> delivery might be available

       |  total | destination            |  cost | rapid |

       |  98.00 | Rhode Island           |  8.25 |  yes  |
       | 101.00 | Rhode Island           |  0.00 |  yes  |
       |  99.00 | Kansas                 |  8.25 |  yes  |
       | 101.00 | Kansas                 |  0.00 |  yes  |
       |  99.00 | Hawaii                 |  8.25 |  yes  |
       | 101.00 | Hawaii                 |  8.25 |  yes  |
       | 101.00 | Alaska                 |  8.25 |  yes  |
       |  99.00 | Ontario, Canada        | 40.00 |   no  |
       |  99.00 | Brisbane, Australia    | 55.00 |   no  |
       |  99.00 | London, United Kingdom | 55.00 |   no  |
       |  99.00 | Kuantan, Malaysia      | 55.00 |   no  |
       | 101.00 | Tierra del Fuego       | 55.00 |   no  |

then that Scenario will unroll into a series of scenarios, each with one value from the table inserted into their placeholders <total>, <destination>, and <rapid>. So this step method will receive each line in the “destination” column:

def step_the_order_will_ship_to_(self, location):
    r'the order will ship to (.*)'

(And observe that naming the placeholder the same as the method argument is a reeeally good idea, but naturally unenforceable.)

Morelia will take each line of the table, and construct a complete test case out of the Scenario steps, running unittest.TestCase.setUp() and unittest.TestCase.tearDown() around them.

If you use many tables then Morelia would use permutation of all rows in all tables:

Scenario: orders above $100.00 to the continental US get free ground shipping
  When we send an order totaling $<total>, with a 12345 SKU, to our warehouse
   And the order will ship to <destination>
   And we choose that delivery should be <speed>
        | speed   |

        | rapid   |
        | regular |

  Then the ground shipping cost is $<cost>

       |  total | destination            |  cost |

       |  98.00 | Rhode Island           |  8.25 |
       | 101.00 | Rhode Island           |  0.00 |
       |  99.00 | Kansas                 |  8.25 |

In above example 2 * 3 = 6 different test cases would be generated.

Doc Strings

Docstrings attached to steps are passed as keyword argument _text into method:

Feature: Addition
    In order to avoid silly mistakes
    As a math idiot
    I want to be told the sum of two numbers

Scenario: Add two numbers
    Given I have powered calculator on
    When I enter "50" into the calculator
    And I enter "70" into the calculator
    And I press add
    Then I would see on the screen
        '''
        Calculator example
        ==================
         50
        +70
        ---
        120
        '''
def step_i_would_see_on_the_screen(self, _text):
     pass

# or

def step_i_would_see_on_the_screen(self, **kwargs):
     _text = kwargs.pop('_text')

Morelia is smart enough not to passing this argument if you don’t name it. Below example won’t raise exception:

def step_i_would_see_on_the_screen(self):
     pass

It’ll be simply assumed that you ignore docstring.

Labels

Labels attached to features and scenarios are available as keyword argument _label:

@web
@android @ios
Feature: Addition
    In order to avoid silly mistakes
    As a math idiot
    I want to be told the sum of two numbers

@wip
Scenario: Add two numbers
    Given I have powered calculator on
    When I enter "50" into the calculator
    And I enter "70" into the calculator
    And I press add
    Then the result should be "120" on the screen
def step_I_enter_number_into_the_calculator(self, num, _label):
     pass

As like with doc-strings you can ommit keyword parameter if you don’t need it:

def step_I_enter_number_into_the_calculator(self, num):
     pass

Labels allows you to implement custom logic depending on labels given.

Note

Compatibility

Morelia does not connects any custom logic with labels as some other Behavior Driven Development tools do. You are put in the charge and should add logic if any. If you are looking for ability to selectivly running features and scenarios look at morelia.decorators.tags() decorator.

Matchers Classes

class morelia.matchers.IStepMatcher(suite, step_pattern='^step_')

Bases: object

Matches methods to steps.

Subclasses should implement at least match and suggest methods.

add_matcher(matcher)

Add new matcher at end of CoR.

Parameters:

matcher (IStepMatcher) – matcher to add

Returns:

self

abstract match(predicate, augmented_predicate, step_methods)

Match method from suite to given predicate.

Parameters:
  • predicate (str) – step predicate

  • augmented_predicate (str) – step augmented_predicate

  • step_methods (list) – list of all step methods from suite

Returns:

(method object, args, kwargs)

Return type:

(method, tuple, dict)

suggest(predicate)

Suggest method definition.

Method is used to suggest methods that should be implemented.

Parameters:

predicate (str) – step predicate

Returns:

(suggested method definition, suggested method name, suggested docstring)

Return type:

(str, str, str)

class morelia.matchers.MethodNameStepMatcher(suite, step_pattern='^step_')

Bases: IStepMatcher

Matcher that matches steps by method name.

match(predicate, augmented_predicate, step_methods)

See IStepMatcher.match().

suggest(predicate)

See IStepMatcher.suggest().

class morelia.matchers.ParseStepMatcher(suite, step_pattern='^step_')

Bases: IStepMatcher

Matcher that matches steps by format-like string in docstring.

match(predicate, augmented_predicate, step_methods)

See IStepMatcher.match().

class morelia.matchers.RegexpStepMatcher(suite, step_pattern='^step_')

Bases: IStepMatcher

Matcher that matches steps by regexp in docstring.

match(predicate, augmented_predicate, step_methods)

See IStepMatcher.match().

Formatting output

Morelia complies with Unix’s Rule of Silence [1] so when you hook it like this:

verify(filename, self)

and all tests passes it would say nothing:

$ python -m unittest test_acceptance
.
----------------------------------------------------------------------
Ran 1 test in 0.028s

OK

(here’s only information from test runner)

But when something went wrong it would complie with Unix’s Rule of Repair [2] and fail noisily:

F
======================================================================
FAIL: test_addition (__main__.CalculatorTestCase)
Addition feature.
----------------------------------------------------------------------
AssertionError:
Feature: Addition
    In order to avoid silly mistakes
    As a math idiot
    I want to be told the sum of two numbers
Scenario: Add two numbers
    Then the result should be "120" on the screen

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "./test_acceptance.py", line 31, in test_addition
    verify(filename, self)
  File "(...)/morelia/__init__.py", line 119, in verify
    execute_script(feature, suite, scenario=scenario, config=conf)
  File "(...)/morelia/parser.py", line 75, in execute_script
    raise exc from AssertionError(breadcrumbs)
  File "./test_acceptance.py", line 46, in step_the_result_should_be_on_the_screen
    self.assertEqual(int(number), self.calculator.get_result())
AssertionError: 120 != 121

----------------------------------------------------------------------
Ran 1 test in 0.020s

FAILED (failures=1)

Verbosity

In Behaviour Driven Development participate both programmers and non-programmers and the latter are often used to programs which report all the time what is going on. So to make Morelia a little more verbose you can configure custom output in pyproject.toml file. E.g.

[tool.morelia.default.output]
formatter.format="text"
writer.type="terminal"
verify(filename, self)
Feature: Addition
    In order to avoid silly mistakes
    As a math idiot
    I want to be told the sum of two numbers
Scenario: Add two numbers
    Given I have powered calculator on                       # pass  0.000s
    When I enter "50" into the calculator                    # pass  0.000s
    And I enter "70" into the calculator                     # pass  0.000s
    And I press add                                          # pass  0.001s
    Then the result should be "120" on the screen            # pass  0.001s
Scenario: Subsequent additions
    Given I have powered calculator on                       # pass  0.000s
    When I enter "50" into the calculator                    # pass  0.000s
    And I enter "70" into the calculator                     # pass  0.000s
    And I press add                                          # pass  0.001s
    And I enter "20" into the calculator                     # pass  0.000s
    And I press add                                          # pass  0.001s
    Then the result should be "140" on the screen            # pass  0.001s
.
----------------------------------------------------------------------
Ran 1 test in 0.027s

OK

Formatter Classes

class morelia.formatters.Buffered(wrapped: IOutput)

Bases: IOutput

class morelia.formatters.FileOutput(path, open_func=<built-in function open>)

Bases: IOutput

class morelia.formatters.IOutput

Bases: ABC

class morelia.formatters.RemoteOutput(url, transport=<module 'requests' from '/home/docs/checkouts/readthedocs.org/user_builds/morelia/envs/latest/lib/python3.12/site-packages/requests/__init__.py'>)

Bases: IOutput

class morelia.formatters.TerminalOutput(dest='stderr')

Bases: FileOutput

class morelia.formatters.TextFormat(output: IOutput, color=False)

Bases: VisitorObserver

class morelia.formatters.Writer(formatter)

Bases: VisitorObserver

Decorators

Sometimes you need selectively run tests. For that reason you can tag your tests:

# test_acceptance.py

import unittest

from morelia import run
from morelia.decorators import tags


class CalculatorTestCase(unittest.TestCase):

    @tags(['basic'])
    def test_addition(self):
        ''' Addition feature '''
        filename = os.path.join(os.path.dirname(__file__), 'add.feature')
        verify(filename, self)
        # ...

    @tags(['advanced'])
    def test_substraction(self):
        ''' Substraction feature '''
        filename = os.path.join(os.path.dirname(__file__), 'substract.feature')
        verify(filename, self)
        # ...

    @tags(['slow', 'advanced'])
    def test_multiplication(self):
        ''' Multiplication feature '''
        filename = os.path.join(os.path.dirname(__file__), 'multiplication.feature')
        verify(filename, self)
        # ...

And run tests only for selected features:

$ MORELIA_TAGS=basic python -m unittest test_acceptance

.ss
----------------------------------------------------------------------
Ran 3 test in 0.018s

OK (skipped=2)

$ MORELIA_TAGS=advanced python -m unittest test_acceptance

s..
----------------------------------------------------------------------
Ran 3 test in 0.048s

OK (skipped=2)

$ MORELIA_TAGS=-slow python -m unittest test_acceptance

..s
----------------------------------------------------------------------
Ran 3 test in 0.028s

OK (skipped=1)

$ MORELIA_TAGS=advanced,-slow python -m unittest test_acceptance

s.s
----------------------------------------------------------------------
Ran 3 test in 0.022s

OK (skipped=2)
morelia.decorators.tags(tags_list)

Skip decorated test methods or classes if tags matches.

Parameters:
  • tags_list (list) – list of tags for test

  • config (morelia.config.Config) – optional configuration object

Configuration

Morelia looks for configuration file “pyproject.toml” in current directory. If it is not present then it looks for “~/.config/morelia/config.toml”.

You can bypass that setting environment variable MORELIA_CONFIG to the path to config file.

Morelia reads it’s configuration from “tool.morelia.<section>” namespace. You can give section part when calling “verify” method.

E.g. if you have configuration:

[tool.morelia.default]
wip=false

[tool.morelia.myconfig]
wip=true

and call “verify” as:

verify(filename, self, config="myconfig")

Then it would be run with wip (work in progress) mode active.

If no config is passed, then “default” is assumed.

Footnotes