tests(bdd): Enbrace behavior driven development¶
Description¶
Abstract
========
Explain why BDD is great and install pytest-bdd
Motivation
==========
See this `tweet from Cory House
<https://mobile.twitter.com/housecor/status/1124308540162805761>`_:
Test descriptions should ideally include:
1. The name of the system under test
2. The scenario under test
3. The expected result
Why?
1. It makes the test easier to understand (remember, tests are docs
too)
2. It makes the test easier to fix when it fails
-- @housecor 2019-05-03
Rationale
=========
A good way to have this is to have tests written in a standard way.
Gherkin, the langage for BDD, is a great way to do this.
The test can then be written and understood by anoyone:
.. code-block:: gherkin
Feature: concat function
Scenario: Concatenating two strings
Given a string "foo"
And a string "bar"
When I pass them to the concat function
Then I should get "foobar" in retun
The developer needs to convert it to code. Each step is a (reusable)
function:
.. code-block:: python
from pytest_bdd import scenario, given, when, then
@scenario('concat-function.feature', 'Concatenating two strings')
def test_concat():
pass
@given('a string "foo"')
def string_foo():
return 'foo'
@given('a string "bar"')
def string_bar():
return 'bar'
@when('I pass them to the concat function')
def concat_foo_and_bar(string_foo, string_bar):
return concat(string_foo)
@then('I should get "foobar" in return')
def result_should_be_foobar(concat_foo_and_bar):
assert concat_foo_and_bar == 'foobar'
Info¶
- Hash
c3d850c1fe495dd31e2277f670cc90e823e0b593
- Date
2019-06-03 13:55:01 +0200
- Parents
docs(rtd): Add configuration for readthedocs.org [0ba93b9a] — 2019-06-03 13:55:00 +0200
- Children
tests: Remove pure testing tests [3fcf835f] — 2019-06-03 15:50:46 +0200
- Branches
- Tags
(No tags)
Changes¶
README.rst¶
- Type
Modified
- Stats
+136 -0
@@ -247,6 +247,142 @@ And I want to do things a little bit differently that we are used too.
I *may* use TDD (`Test-Driven Development <https://en.wikipedia.org/wiki/Test-driven_development>`_), but it's not sure yet, as I'm really not used to it. Will see.
+But...
+
+See this `tweet from Cory House <https://mobile.twitter.com/housecor/status/1124308540162805761>`_:
+
+ Test descriptions should ideally include:
+ 1. The name of the system under test
+ 2. The scenario under test
+ 3. The expected result
+
+ Why?
+ 1. It makes the test easier to understand (remember, tests are docs too)
+ 2. It makes the test easier to fix when it fails
+
+ -- @housecor 2019-05-03
+
+How to do this?
+
+Generally a test function is written this way:
+
+.. code-block:: python
+
+ def test_concat():
+ assert concat('foo', 'bar') == 'foobar'
+
+We can give it a bette name:
+
+.. code-block:: python
+
+ def test_concat_should_concat_two_strings():
+ assert concat('foo', 'bar') == 'foobar'
+
+And add a docstring:
+
+.. code-block:: python
+
+ def test_concat_should_concat_two_strings():
+ """The ``concat`` function should concat the two given strings."""
+ assert concat('foo', 'bar') == 'foobar'
+
+We still don't respect what's said in the tweet.
+
+And also there is no formalism.
+
+What if we can say something like:
+
+ Given the strings "foo" and "bar", when I pass them to the concat function, then it should return "foobar".
+
+So let's use it:
+
+.. code-block:: python
+
+ def test_concat_should_concat_two_strings():
+ """Given the strings "foo" and "bar", when I pass them to the concat function, then it should return "foobar"."""
+ assert concat('foo', 'bar') == 'foobar'
+
+It's better.
+
+If this test sounds familiar to you, its normal: it's `Gherkin language <https://cucumber.io/docs/gherkin/reference/>`_ and used in BDD (`Behavior-driven development <https://en.wikipedia.org/wiki/Behavior-driven_development>`_)
+
+BDD is generally used for functional tests. But I want to test if it can be applied at other levels: unit tests and integration tests.
+
+So let's use `pytest-bdd <https://github.com/pytest-dev/pytest-bdd>`_ to write this test.
+
+First, the Gherkin, in a file `concat-function.feature`:
+
+.. code-block:: gherkin
+
+ Feature: concat function
+
+ Scenario: Concatenating two strings
+ Given a string "foo"
+ And a string "bar"
+ When I pass them to the concat function
+ Then I should get "foobar" in retun
+
+Then the test:
+
+.. code-block:: python
+
+ from pytest_bdd import scenario, given, when, then
+
+ @scenario('concat-function.feature', 'Concatenating two strings')
+ def test_concat():
+ pass
+
+ @given('a string "foo"')
+ def string_foo():
+ return 'foo'
+
+ @given('a string "bar"')
+ def string_bar():
+ return 'bar'
+
+ @when('I pass them to the concat function')
+ def concat_foo_and_bar(string_foo, string_bar):
+ return concat(string_foo)
+
+ @then('I should get "foobar" in return')
+ def result_should_be_foobar(concat_foo_and_bar):
+ assert concat_foo_and_bar == 'foobar'
+
+It can seems cumbersome, but we can:
+ - use the great power of pytest fixtures
+ - parametrize the strings
+ - use the parametrize feature of pytest to run a scenario for different inputs
+
+I won't go further here on how to make this code less cumbersome and more reusable, but as all my tests will be written this way, you'll see a lot of examples.
+
+Among the benefits:
+ - the "features", ie the tests (in Gherkin) are readable/writeable by everyone
+ - we can write our "features" in advance: we have the specifications and documentation
+ - we have a formal way of describing what we test and how we do it.
+ - every part does one thing so easy to correct if needed
+
+The main (and only one, in my opinion) inconvenient is that tests may be longer to write. But we cannot have some advantages without losing something, and for me it's something I can live with.
+
+And you'll be surprised to see this "BDD" thing used in very unusual case. An example?
+
+.. code-block:: gherkin
+
+ Feature: Describing a Repository
+
+ Scenario: A Repository has a name
+ Given a Repository
+ Then it must have a field named name
+
+ Scenario: A Repository name is a string
+ Given a Repository
+ Then its name must be a string
+
+ Scenario: A Repository name cannot be None
+ Given a Repository
+ Then its name cannot be None
+
+Yes, things like that :)
+
Git
===
setup.cfg¶
- Type
Modified
- Stats
+1 -0
@@ -35,6 +35,7 @@ dev =
wheel
tests =
pytest
+ pytest-bdd
pytest-cov
pytest-sugar
lint=
test_testing.feature¶
- Type
Added
- Stats
+5 -0
@@ -0,0 +1,5 @@
+Feature: Testing bdd tests
+
+ Scenario: A bdd test must pass
+ Given a bdd test
+ Then it must pass
test_testing.py¶
- Type
Modified
- Stats
+17 -0
@@ -1,7 +1,24 @@
"""Test file to check testing tools"""
+from pytest_bdd import given, parsers, scenario, then
+
def test_passing():
"""A test that must pass"""
assert 1 == 1
+
+
+@scenario("test_testing.feature", "A bdd test must pass")
+def test_bdd():
+ pass
+
+
+@given("a bdd test")
+def a_bdd_test():
+ return "a bdd test"
+
+
+@then("it must pass")
+def it_must_pass(a_bdd_test):
+ assert a_bdd_test == "a bdd test"