Migrate from pytest-bdd

pytest-bdd is an alternative to behave that works with pytest.

It’s quite similar to tursu in the idea, it is more mature, but its also more verbose and the approach is different.

pytest-bdd does not compile to AST, so while using pdb, it’s harder to navigate and now in which step the scenario is currently. In case of failure, the context provided by pytest-bdd is minimal.

Also note that in tursu, you can provide the scenario file in the cli command as a test, not a test_*.py file.

# using pytest-bdd
uv run pytest --trace -sxv tests/functionals/test_scenario_binding_file.py

# using tursu
uv run pytest --trace -sxv tests/functionals/my_gherkin.scenario

Step 1 - Install tursu and configure it.

Installation:

uv add --group dev tursu

Add the Tursu Gherkin compiler to AST generate the tests suite:

  • Ensure the __init__.py file exists in the functionals tests suite.

touch tests/functionals/__init__.py
cat << 'EOF' > tests/functionals/conftest.py
from tursu.plugin import tursu_collect_file

tursu_collect_file()
EOF
  • remove all the scenario/scenarios call of pytest_bdd

diff --git a/tests/test_login.py b/tests/test_login.py
index c373bd4..63c4b46 100644
--- a/tests/test_login.py
+++ b/tests/test_login.py
@@ -1,4 +1,4 @@
-from pytest_bdd import scenarios, given, when, then, parsers
+from pytest_bdd import given, when, then, parsers


 from .conftest import DummyApp
@@ -27,6 +27,3 @@ def assert_connected(app: DummyApp, username: str):
 @then(parsers.parse("I am not connected"))
 def assert_not_connected(app: DummyApp):
     assert app.connected_user is None
-
-
-scenarios("scenario")

Step 2

In pytest-bdd, the given function create the fixtures, where in tursu,

you create the fixture, given step manipulate it.

Futhermore, remove parsers.parse, use plain string.

pytest-bdd

from pytest_bdd import given, parsers

@given(
    parsers.parse("a user {username} with password {password}"),
    target_fixture="app",
)
def give_user(username: str, password: str) -> DummyApp:
    app = DummyApp()
    app.users[username] = password
    return app

tursu

from tursu import given


@pytest.fixture()
def app() -> DummyApp:
    return DummyApp()


@given("a user {username} with password {password}")
def give_user(app: DummyApp, username: str, password: str):
    app.users[username] = password

Step 3 - cleanup all the when and then decorators

pytest-bdd

from pytest_bdd import given, when, then, parsers

@when(parsers.parse("{username} login with password {password}"))
def login(app: DummyApp, username: str, password: str):
    app.login(username, password)


@then(parsers.parse("I am connected with username {username}"))
def assert_connected(app: DummyApp, username: str):
    assert app.connected_user == username

Note

If you are using extra_types, remove it too, tursu matcher uses python type hinting,

refer to the pattern matcher documentation for help.

tursu

from tursu import then, when

@when("{username} login with password {password}")
def login(app: DummyApp, username: str, password: str):
    app.login(username, password)


@then("I am connected with username {username}")
def assert_connected(app: DummyApp, username: str):
    assert app.connected_user == username

Step 4 - clean the names

Remove all the test_ prefix of the steps file, test_ file are entirely

generated by tursu before runing the tests, and cleaned up at the end.

You can arrange all your steps in your testing directory by the way.

Step 5 - Run the test, enable tracing!

It’s hard to run a debugger in behave, remote debugging is mandatory, in pytest, you can use start a debugger in your test by a

uv run pytest --trace -sxv tests/functionals/