Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/spyoungtech/behave-classy
Class-based step implementations for the Python behave BDD framework
https://github.com/spyoungtech/behave-classy
bdd behave
Last synced: 2 months ago
JSON representation
Class-based step implementations for the Python behave BDD framework
- Host: GitHub
- URL: https://github.com/spyoungtech/behave-classy
- Owner: spyoungtech
- License: mit
- Created: 2018-08-10T14:47:33.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2019-12-21T01:29:27.000Z (about 5 years ago)
- Last Synced: 2024-09-16T23:32:58.792Z (3 months ago)
- Topics: bdd, behave
- Language: Python
- Size: 13.7 KB
- Stars: 16
- Watchers: 5
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: readme.md
- License: LICENSE
Awesome Lists containing this project
README
# behave-classy
## Installation
Installation is made easy with `pip`
```
pip install behave-classy
```## What is 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
Usage is fairly simple and follows these basic steps:
- Use the `step_impl_base` to 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 `register` method of your class.# Example
Consider some step library which provides a set of steps in the class `BankAccountSteps`.
```python
# some_library/steps.py
from behave_classy import step_impl_baseBase = 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.
```python
# myproject/features/steps/mysteps.py
from some_library.steps import BankAccountSteps
BankAccountSteps().register()
```Then with a typical feature file...
```gherkin
# 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 142
```You can simply run behave as normal
```
$ behaveFeature: 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:32Scenario: 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:321 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
```## extending and integrating existing work
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**:
1. We will add additional steps the class, adding new methods, which reuse existing methods. One of these will use a different matcher, the RegexMatcher.
2. We will *extend* the existing `withdraw` method to first check that the account has sufficient funds, otherwise raising a ValueError.
3. We will *integrate* unittest assertion matchers to the class by using `unittest.TestCase` as a mixin
4. We will *override* the existing `check_balance` method to use `unittest.TestCase` assertions```python
# myproject/features/steps/mysteps.py
from unittest import TestCase
from behave.matchers import RegexMatcher
from some_library.steps import BankAccountSteps as Baseclass 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
```gherkin
# 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 1Scenario: 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 100Scenario: 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:2Background: # 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:45Scenario: 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:45Scenario: 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:64Scenario: 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 != 10Scenario: 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):
File "features\steps\steps.py", line 61, in withdraw
raise ValueError('Insufficient Funds')
ValueError: Insufficient FundsFailing 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:
1. Our additional loan payment steps ran as expected
2. Our step definition `compare_balance`, using the RegexMatcher, correctly matched our steps in the feature file
3. Our override of the `check_balance` method worked and used unittest assertion (evidenced in the failure case)
4. Our extended `withdraw` method was successfully used as expected (demonstrated by the failing case with ValueError)