Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for async steps #629

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 19 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Expand Up @@ -48,6 +48,7 @@ types-setuptools = "^65.5.0.2"
pytest-xdist = "^3.0.2"
coverage = {extras = ["toml"], version = "^6.5.0"}
Pygments = "^2.13.0" # for code-block highlighting
pytest-asyncio = "^0.21.1"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
18 changes: 17 additions & 1 deletion src/pytest_bdd/scenario.py
Expand Up @@ -12,7 +12,9 @@
"""
from __future__ import annotations

import asyncio
import contextlib
import functools
import logging
import os
import re
Expand Down Expand Up @@ -120,6 +122,19 @@ def get_step_function(request, step: Step) -> StepFunctionContext | None:
return None


def ensure_sync(fn):
"""Convert async function to sync function."""
__tracebackhide__ = True
if not asyncio.iscoroutinefunction(fn):
return fn

@functools.wraps(fn)
def wrapper(*args, **kwargs):
return asyncio.run(fn(*args, **kwargs))

return wrapper


def _execute_step_function(
request: FixtureRequest, scenario: Scenario, step: Step, context: StepFunctionContext
) -> None:
Expand Down Expand Up @@ -156,7 +171,8 @@ def _execute_step_function(

request.config.hook.pytest_bdd_before_step_call(**kw)
# Execute the step as if it was a pytest fixture, so that we can allow "yield" statements in it
return_value = call_fixture_func(fixturefunc=context.step_func, request=request, kwargs=kwargs)
step_func = ensure_sync(context.step_func)
return_value = call_fixture_func(fixturefunc=step_func, request=request, kwargs=kwargs)
except Exception as exception:
request.config.hook.pytest_bdd_step_error(exception=exception, **kw)
raise
Expand Down
69 changes: 69 additions & 0 deletions tests/library/test_async.py
@@ -0,0 +1,69 @@
import textwrap


# TODO: Split this test in one that checks that we work correctly
# with the pytest-asyncio plugin, and another that checks that we correctly
# run async steps.
def test_async_steps(pytester):
"""Test parent given is collected.

Both fixtures come from the parent conftest.
"""
pytester.makefile(
".feature",
async_feature=textwrap.dedent(
"""\
Feature: A feature
Scenario: A scenario
Given There is an async object

When I do an async action

Then the async object value should be "async_object"
And [async] the async object value should be "async_object"
And the another async object value should be "another_async_object"
"""
),
)

pytester.makepyfile(
textwrap.dedent(
"""\
from pytest_bdd import given, parsers, scenarios, then, when
import asyncio
import pytest

scenarios("async_feature.feature")

@pytest.fixture
async def another_async_object():
await asyncio.sleep(0.01)
return "another_async_object"

@given("There is an async object", target_fixture="async_object")
async def given_async_obj():
await asyncio.sleep(0.01)
return "async_object"

@when("I do an async action")
async def when_i_do_async_action():
await asyncio.sleep(0.01)

@then(parsers.parse('the async object value should be "{value}"'))
async def the_sync_object_value_should_be(async_object, value):
assert async_object == value

@then(parsers.parse('[async] the async object value should be "{value}"'))
async def async_the_async_object_value_should_be(async_object, value):
await asyncio.sleep(0.01)
assert async_object == value

@then(parsers.parse('the another async object value should be "{value}"'))
def the_another_async_object_value_should_be(another_async_object, value):
assert another_async_object == value

"""
)
)
result = pytester.runpytest("--asyncio-mode=auto")
result.assert_outcomes(passed=1)