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 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:
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.
- 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)¶
- 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.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.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