Installation is made easy with pip
pip install behave-classy
beahve-classy provides a class-based API for behave step implementations
This package is geared towards authors of step implementation libraries and aims to provide a more flexible and extensible interface for behave step libraries and their users.
The primary features
- ability to define step definitions as classes
- ability to extend steps from your own classes (or perhaps classes provided by other libraries/packages)
- ability to define a matcher per-method without changing global state of the 'current matcher'
- wraps methods to transform context into an attribute (
self.context), so it's not necessary to have each method use context as first parameter in the signature. Works with default behave runner.
Usage is fairly simple and follows these basic steps:
- Use the
step_impl_baseto create a base class that will contain your step definitions in its own local registry. - Make your subclass and definitions
- When you want to use your steps, simply call the
registermethod of your class.
Consider some step library which provides a set of steps in the class BankAccountSteps.
# some_library/steps.py
from behave_classy import step_impl_base
Base = step_impl_base()
class BankAccountSteps(Base):
@Base.given(u'I have a balance of {amount:d}')
def set_balance(self, amount):
self.balance = amount
@Base.when(u'I deposit {amount:d} into the account')
def deposit(self, amount):
if amount < 0:
raise ValueError('Deposit amounts cannot be negative')
self.balance += amount
@Base.when(u'I withdraw {amount:d} from the account')
def withdraw(self, amount):
self.balance -= amount
@Base.then(u'the balance should be {expected_amount:d}')
def check_balance(self, expected_amount):
assert self.balance == expected_amount
@property
def balance(self):
"""convenience shortcut for context balance"""
amount = getattr(self.context, 'balance', 0)
return amount
@balance.setter
def balance(self, new_amount):
"""convenience setter"""
self.context.balance = new_amount
As a user of such a library, you would import the class, then register it, so its definitions are added to the global step registry used by behave.
# myproject/features/steps/mysteps.py
from some_library.steps import BankAccountSteps
BankAccountSteps().register()Then with a typical feature file...
# myproject/features/account_balance.feature
Feature: bank account balance
Background:
Given I have a balance of 100
Scenario: withdraw
When I withdraw 42 from the account
Then the balance should be 58
Scenario: deposit
When I deposit 42 into the account
Then the balance should be 142You can simply run behave as normal
$ behave
Feature: bank account balance # features/account_balance.feature:2
Background: # features/account_balance.feature:3
Scenario: withdraw # features/account_balance.feature:6
Given I have a balance of 100 # features/steps/steps.py:18
When I withdraw 42 from the account # features/steps/steps.py:28
Then the balance should be 58 # features/steps/steps.py:32
Scenario: deposit # features/account_balance.feature:10
Given I have a balance of 100 # features/steps/steps.py:18
When I deposit 42 into the account # features/steps/steps.py:22
Then the balance should be 142 # features/steps/steps.py:32
1 feature passed, 0 failed, 0 skipped
2 scenarios passed, 0 failed, 0 skipped
6 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.002s
Without behave-classy, you would typically use behave's context.execute_steps feature to extend on work. Although
this works for simple cases, it can be quite inflexible, especially once your cases become nontrivial. behave-classy allows you to use usual python techniques (e.g. subclassing, method extensions/overriding, etc.) to reuse
your existing code, just like you would do in any other python code.
Consider the Bank Account example from the previous section. This example demonstrates several features:
- We will add additional steps the class, adding new methods, which reuse existing methods. One of these will use a different matcher, the RegexMatcher.
- We will extend the existing
withdrawmethod to first check that the account has sufficient funds, otherwise raising a ValueError. - We will integrate unittest assertion matchers to the class by using
unittest.TestCaseas a mixin - We will override the existing
check_balancemethod to useunittest.TestCaseassertions
# myproject/features/steps/mysteps.py
from unittest import TestCase
from behave.matchers import RegexMatcher
from some_library.steps import BankAccountSteps as Base
class ExtendedSteps(Base, TestCase):
def withdraw(self, amount):
"""Extends withdraw method to make sure enough funds are in the account, then calls withdraw from superclass"""
if amount > self.balance:
raise ValueError('Insufficient Funds')
super().withdraw(amount)
def check_balance(self, expected_amount):
"""Override check_balance method to use unittest assertions instead"""
self.assertEquals(self.balance, expected_amount)
@Base.when(u'I pay my {loan_name} loan payment')
def pay_loan(self, loan_name):
"""additional when step for loan payments"""
loan_payments = {
'student': 600,
'car': 200
}
payment_amount = loan_payments[loan_name]
self.withdraw(payment_amount)
@Base.then(u'the balance should be (less|greater) than (or equal to )*(\d+)', matcher=RegexMatcher)
def compare_balance(self, operator, or_equals, amount):
"""Additional step using regex matcher to compare the current balance with some number"""
amount = int(amount)
if operator == 'less':
if or_equals:
self.assertLessEqual(self.balance, amount)
else:
self.assertLess(self.balance, amount)
elif or_equals:
self.assertGreaterEqual(self.balance, amount)
else:
self.assertGreater(self.balance, amount)
ExtendedSteps().register()We'll add some additional scenarios to our feature file to show off these new alterations
# myproject/features/account_balance.feature
# ...
Scenario: pay loans
Given I have a balance of 1000
When I pay my student loan payment
Then the balance should be less than 1000
When I pay my car loan payment
Then the balance should be greater than or equal to 1
Scenario: failing unittest assertion (expected to fail)
# this is expected to fail, to show off the unittest assertion at work
Given I have a balance of 100
Then the balance should be greater than 100
Scenario: withdrawing more than balance raises ValueError (expected to fail)
# this is expected to fail with a ValueError to show off the extended withdraw method
Given I have a balance of 100
When I withdraw 500 from the account
Then if we run this using behave...
$ behave
Feature: bank account balance # features/account_balance.feature:2
Background: # features/account_balance.feature:3
Scenario: withdraw # features/account_balance.feature:6
Given I have a balance of 100 # features/steps/steps.py:25
When I withdraw 42 from the account # features/steps/steps.py:58
Then the balance should be 58 # features/steps/steps.py:45
Scenario: deposit # features/account_balance.feature:10
Given I have a balance of 100 # features/steps/steps.py:25
When I deposit 42 into the account # features/steps/steps.py:29
Then the balance should be 142 # features/steps/steps.py:45
Scenario: pay loans # features/account_balance.feature:14
Given I have a balance of 100 # features/steps/steps.py:25
Given I have a balance of 1000 # features/steps/steps.py:25
When I pay my student loan payment # features/steps/steps.py:48
Then the balance should be less than 1000 # features/steps/steps.py:64
When I pay my car loan payment # features/steps/steps.py:48
Then the balance should be greater than or equal to 1 # features/steps/steps.py:64
Scenario: failing unittest assertion (expected to fail) # features/account_balance.feature:21
Given I have a balance of 100 # features/steps/steps.py:25
Given I have a balance of 100 # features/steps/steps.py:25
Then the balance should be 10 # features/steps/steps.py:45
Assertion Failed: 100 != 10
Scenario: withdrawing more than balance raises ValueError (expected to fail) # features/account_balance.feature:27
Given I have a balance of 100 # features/steps/steps.py:25
Given I have a balance of 100 # features/steps/steps.py:25
When I withdraw 500 from the account # features/steps/steps.py:58
Traceback (most recent call last):
<partially omitted for readme brevity>
File "features\steps\steps.py", line 61, in withdraw
raise ValueError('Insufficient Funds')
ValueError: Insufficient Funds
Failing scenarios:
features/account_balance.feature:21 failing unittest assertion (expected to fail)
features/account_balance.feature:27 withdrawing more than balance raises ValueError (expected to fail)
0 features passed, 1 failed, 0 skipped
3 scenarios passed, 2 failed, 0 skipped
16 steps passed, 2 failed, 0 skipped, 0 undefined
Took 0m0.008s
We can see our additional scenarios ran and observe the following from the results:
- Our additional loan payment steps ran as expected
- Our step definition
compare_balance, using the RegexMatcher, correctly matched our steps in the feature file - Our override of the
check_balancemethod worked and used unittest assertion (evidenced in the failure case) - Our extended
withdrawmethod was successfully used as expected (demonstrated by the failing case with ValueError)