Migrate from behave¶
This framework is a modern, type-safe alternative to Behave, designed to bring type hints, better IDE support, and static analysis to Behavior-Driven Development (BDD) in Python 3.
It improves upon Behave by eliminating dynamic attribute issues and enhancing maintainability.
The cherry on top is that you can run the debugger without pain.
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
create a minimal conftest.py
cat << 'EOF' > tests/functionals/conftest.py
from tursu.plugin import tursu_collect_file
tursu_collect_file()
EOF
## Step 2 - Replace context with fixtures in all decorators
Tursu simplifies BDD by replacing Behave’s dynamic context with pytest fixtures,
making it more maintainable and test-friendly.
Example:
### Behave
```python
from behave import given
@given("A user with username {username} and password {password}")
def step_create_user(context, username, password):
context.user = {"username": username, "password": password}
@when("they log in")
def step_login(context):
user = context.user
context.logged_in = user["username"] == "john" and user["password"] == "secret"
@then("they should see a welcome message")
def step_welcome_message(context):
assert context.logged_in, "Login failed!"
Tursu¶
from dataclasses import dataclass
import pytest
from tursu import given
@dataclass
class User:
username: str
password: str
logged_in: bool = False
@pytest.fixture
def user() -> User:
return User(username="", password="")
@given("a user with username {username} and password {password}")
def step_create_user(username: str, password: str, user: User):
user.username = username
user.password = password
@when("they log in")
def step_login(user: User):
user.logged_in = user.username == "john" and user.password == "secret"
@then("they should see a welcome message")
def step_welcome_message(user: User):
assert user.logged_in, "Login failed!"
Note
context.text has to be replaced by doc_string
context.table has to be replaced by data_table
Behave¶
@given('a set of specific users')
def step_impl(context):
for row in context.table:
context.model.add_user(name=row['name'], department=row['department'])
@then('I will see the account details')
def step_impl(context):
assert context.text == ...
Tursu¶
@given('a set of specific users')
def step_impl(model: MyModelFixture, data_dable: list[dict[str, str]]):
for row in data_dable:
model.add_user(name=row['name'], department=row['department'])
@then('I will see the account details')
def step_impl(doc_string: text):
assert doc_string == ...
Step 3 - Replace Behave Fixtures with Pytest autouse Fixtures¶
Behave allows using fixtures through before_scenario and after_scenario. In Tursu, we achieve the same behavior using pytest’s autouse fixtures.
Example:
Behave¶
from behave import fixture, use_fixture
from my_dummy_app.entrypoint import main
from .utils import wait_for_url
@fixture
def start_webapp(context, **kwargs):
proc = Process(target=main, daemon=True)
proc.start()
wait_for_url("http://localhost:8888")
yield
proc.kill()
def before_scenario(context, scenario):
use_fixture(start_webapp, context)
Tursu¶
import pytest
from my_dummy_app.entrypoint import main
from .utils import wait_for_url
@pytest.fixture(autouse=True, scope="function") # adapt the scope for your needs
def start_webapp():
proc = Process(target=main, daemon=True)
proc.start()
wait_for_url("http://localhost:8888")
yield
proc.kill()
Step 4 - Remove code that launch a browser, use pytest-playwright¶
Example:
Behave¶
@fixture
def browser(context: Any, **kwargs: Any) -> Iterator[None]:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, slow_mo=50)
context.browser = browser.new_page(base_url="http://localhost:8888")
yield
browser.close()
Tursu¶
# you can configure the browser context in a fixture
# https://playwright.dev/python/docs/test-runners#use-custom-viewport-size
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
return {
**browser_context_args,
"viewport": {
"width": 1920,
"height": 1080,
}
}
Step 5 - Declare your gherkin tag into your pytest marker.¶
This is highly recommended, to avoid warnings from pytest.
# pyproject.toml
[tool.pytest.ini_options]
markers = [
"dev: experiment with some background context.",
"openapi: open api tests.",
"wip: work in progress.",
]
Step 6 - 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 \
-m "wip" \
--base-url http://localhost:8888 \
--headed \
--browser chromium \
--trace \
-sxv \
tests/functionals/
-m “wip” : only run the @wip Gherkin tag
–base-url http://localhost:8888 : configure a base url, may be in the code
–headed : start a real browser to see what happen
–browser chromium : choose the browser you want
–trace : start the test by a breakpoint
-sv : donc capture stdout, and choose your vebosity level
$ uv run pytest \
-m "wip" \
--base-url http://localhost:8888 \
--headed \
--browser chromium \
--trace \
-sxv \
tests/using_playwright
================================= test session starts =================================
baseurl: http://localhost:8888
configfile: pyproject.toml
plugins: cov-6.0.0, playwright-0.7.0, base-url-2.1.0
collected 1 item
tests/using_playwright/test_1_Basic_Test.py::test_2_Hello_world[chromium]
>>>>>>>>>>>>>> PDB runcall (IO-capturing turned off for fixture capsys) >>>>>>>>>>>>>>>
> ~/git/tursu/tests/using_playwright/test_1_Basic_Test.py(10)test_2_Hello_world()
-> with TursuRunner(request, capsys, tursu, ['📄 Document: 01_basic.feature', '🥒 Feature: Basic Test', '🎬 Scenario: Hello world']) as tursu_runner:
📄 Document: 01_basic.feature
🥒 Feature: Basic Test
🎬 Scenario: Hello world
> ~/git/tursu/tests/using_playwright/test_1_Basic_Test.py(11)test_2_Hello_world()
-> tursu_runner.run_step('given', 'anonymous user on /', page=page)
(Pdb) c
>>>>>>>>>>>>>>> PDB continue (IO-capturing resumed for fixture capsys) >>>>>>>>>>>>>>>>
✅ Given anonymous user on /
✅ Then I see the text "Hello, World!"
127.0.0.1 - - [14/Mar/2025 21:51:08] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [14/Mar/2025 21:51:08] "GET /favicon.ico HTTP/1.1" 200 -
PASSED
================================== 1 passed in 6.52s ==================================