Tutorial¶
Prerequisites¶
Create a feature file¶
Find a place to store tests. Django docs suggest a tests directory for each app.
Feature files are written in the Gherkin format. Paste the following into tests/example.feature:
Feature: BDD example
Scenario: Eating apples
Given I have 8 apples
When I eat 2 apples
Then I have 6 apples left
Run Django’s test command:
$ ./manage.py test
...
UnimplementedScenariosError:
* djangoproject/app/tests/example.feature
* Eating apples
...
Ah. We haven’t implemented our steps yet.
Let’s start off with a barebones test case. We could do that manually, but it’s easier to...
Run the code generator¶
$ ./manage.py bddgen
Generated code:
* djangoproject/app/tests/test_example.py
This creates tests/test_example.py:
from swanson import TestCase, step, given, when, then
class BDDExampleTestCase(TestCase):
def test_eating_apples(self):
self.run_scenario(u'Eating apples')
@given(r'(?i)^I have 8 apples$')
def given_i_have_8_apples(self, step):
assert False
@when(r'(?i)^I eat 2 apples$')
def when_i_eat_2_apples(self, step):
assert False
@then(r'(?i)^I have 6 apples left$')
def then_i_have_6_apples_left(self, step):
assert False
Note the assert False lines - these cause the test to fail. Let’s replace them with something more useful:
@given(r'(?i)^I have 8 apples$')
def given_i_have_8_apples(self, step):
self.apples = 8
@when(r'(?i)^I eat 2 apples$')
def when_i_eat_2_apples(self, step):
self.apples -= 2
@then(r'(?i)^I have 6 apples left$')
def then_i_have_6_apples_left(self, step):
self.assertEqual(self.apples, 6)
(We use assertEqual in the last step - that’s one of the built-in unittest assertions.)
Run the tests again:
$ ./manage.py test
...
Ran 2 tests in 0.005s
OK
...
Nice!
Still, our test case isn’t very smart. If we change the number of apples in the feature file, we also need to change the implementation.
What if we had...
Step parameters¶
Budget cuts require we start with 7 apples. Update the feature file:
Given I have 7 apples
When I eat 2 apples
Then I have 5 apples left
And re-run the tests...
$ ./manage.py test
...
StepError: Error running 'Given I have 7 apples' on line 3 of djangoproject/app/tests/example.feature:
NoStepHandlers: No step handlers found for 'Given I have 7 apples'
...
Dang. We could update the Python code to match, but what would that teach us? Let’s try using step parameters.
Take a look at r'(?i)^I have 8 apples$'. That’s a regular expression, a mini-language for matching text against patterns.
Change it to accept a variable number of apples:
@given(r'(?i)^I have (.+) apples$')
def given_i_have_x_apples(self, step):
Note we’ve also updated the function definition - it takes captured text from the regular expression. apples is passed as a string, so convert it to an integer:
@given(r'(?i)^I have (.+) apples$')
def given_i_have_x_apples(self, step):
apples = int(apples)
To share apples with later steps, store it on self:
@given(r'(?i)^I have (.+) apples$')
def given_i_have_x_apples(self, step):
self.apples = int(apples)
Our given step doesn’t do a lot, but that’s ok:
- Given steps set up the test.
- When steps perform an action.
- Then steps check that everything is as expected.
Now, update the other steps:
@when(r'(?i)^I eat (.+) apples$')
def when_i_eat_x_apples(self, step, eat_apples):
self.apples -= int(eat_apples)
@then(r'(?i)^I have (.+) apples left$')
def then_i_have_x_apples_left(self, step, expected_apples):
self.assertEqual(self.apples, int(expected_apples))
And run the tests:
$ ./manage.py test
...
Ran 2 tests in 0.005s
OK
...
Boom. BDD test implemented.