-
Notifications
You must be signed in to change notification settings - Fork 221
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
Async Step Definitions #223
Comments
what about async then and async given? does it even make sense? |
will this work for you? async def send_cucumbers(): @when('i send cucumbers ') |
@olegpidsadnyi This works, however, in my case all step definitions are async (and can call multiple times into async functions). so this would mean a lot of calls to run_until_complete. A generic solution would be to have the ability to easily wrap any step definition with a function. (similar to the before and after hooks). And then it would be great if support of async functions would be provided out of the box. Especially, after reading the discussion about implicit and explicit eventloops in the thread here: https://groups.google.com/forum/#!msg/python-tulip/yF9C-rFpiKk/tk5oA3GLHAAJ seems to be tending towards implicit loops for python code that runs on 3.5.3+ and/or 3.6+. So code that explicitly calls run_until_complete to run async functions will look more and more awkward. |
@jeduden this means pytest-bdd has to depend on yet another library asyncio? I don't really see how a gherkin scenario can by async. If the whole point that it has not to break i think you need to extend your testing suite with some kind of support for async functions (decorator is a good idea). I think semantically you can't really represent async process in step-by-step imperative Gherkin. Therefore it should be explicit. If you have to initiate few async messages - they should stand behind one When step that describes this exercise in a form that humans understand. Gherkin is not a programming language, it is the way to describe steps to humans which can't do async computation in their brains. What do you think, @bubenkoff ? |
well, the async question comes down to the pytest, not specifically to pytest-bdd. |
@jeduden could you show your decorator implementation for when step that is awaiting? |
FYI: there's a helper apparently to minimize the efforts: https://pypi.python.org/pypi/pytest-asyncio |
Support on pytest side is fine. We are using aiohttp test-support and with pytest-asyncio 0.8.0 we can also the helpers from that package. However, these helpers work on scenario level. The step_definitions are one level below, and since the calling code here: https://github.com/pytest-dev/pytest-bdd/blob/master/pytest_bdd/scenario.py#L137 In order to fix this, we currently need to wrap all steps with decorators like this:
|
@olegpidsadnyi declaring a function async doesn't mean it is not imperative. Async functions are imperative like synchronous functions. However, with await / yield from you explicitly define points where the execution of other scheduled coroutines is allowed. In order to schedule the parallel execution of multiple asynchronous functions you would use helpers like asyncio.gather ( see https://docs.python.org/3/library/asyncio-task.html#example-parallel-execution-of-tasks ) The reason why we need to declare our step_definitions async is because we are using the aiohttp client inside to perform networking calls as part of our step definitions. |
As i see from the pytest-asyncio code, it just awaits for every 'async' fixture, so there's no real parallelism possible between the fixtures |
But i do see your point about the need of the wrapper everywhere - it sucks. |
@bubenkoff even that is not possible since there is no hook that allows me to insert a custom decorator on the functions. even if pytest-bdd doesn't support async step definitions out of the box. A hook for wrapping a step definitions would help to reduce duplication. |
but I meant to change pytest-bdd itself to support that automatically |
are you up for making a PR which will add |
@bubenkoff In general, I am happy to make a PR. One question regarding the pytest-asyncio dependency. You mention it because we need to have a library to provide the loop fixture, right ? |
@jeduden yes, also to keep as much |
@bubenkoff i did a quick check on the current test-suite; how i can best write some this for this new type of possible step functions. do you have a pointer for me where i best can add tests ? Or should i create a complete new file ? |
You can put a new file here tests/steps/test_async.py copying the approach of tests/steps/test_unicode.py
|
For what its worth, I took the approach with #221 and then added that hook implementation as a separate pytest plugin that I can install as necessary. |
Is your pytest plugin available anywhere, just so I can try it out? Do you plan on maintaining it? My gut feeling is that integrating the functionality into |
First off, thank you so much for this awesome library! I was hoping I can contribute my experience with @olegpidsadnyi sad here:
Depends on how you look at it.
Agreed, but the cool thing is that despite the name, |
Behave 1.2.6 added some decorators to Testing asyncio Frameworks . That may be inspirational to implement something similar for pytest-bdd. |
Is there a guide somewhere to using pytest-bdd with pytest-asyncio ? I'm not sure how to make them play nicely together and actually execute my bdd tests. |
@bubenkoff Hi! Are you planning to close this PR? |
Just shooting this question as I am stuck with the same issue for handling async step definitions as pytest-asyncio cannot be used here. Is there some update or any way to handle the same as of now? Hope you will please help if some new changes/ways to handle are present. |
Most of web development nowadays is moving to |
I forked the project and applied PR. I was able to take advantage of asynchronous tests. But as time passes, I see that this is unnecessary. If you write integration tests, then nothing prevents you from using requests as a client. I will support the voiced idea that adding asynchrony is redundant and solves other problem. |
@DjaPy but what about if your integration tests are part of your whole asyncio project with lots of async tests? Isn't it's better to just run Currently, I actually do as you suggested, by separating bdd tests (using requests in them) and other tests. However again, for local development, for CI/CD I always need to run two commands instead of one. I mean it's not probably a "must-have" feature, but definitely "nice-to-have" for pytest-bdd. BTW want to give my warm thanks for an awesome library 🚀 |
This is an argument. When you're used to writing in pytest and you don't want to dive into behave. I'll choose pytest-bdd. Yet here another problem is solved. The problem of infrastructure. |
Do we have a workaround getting async to work with pytest-bdd with aiohttp with |
Is there some update about this? It would be a great implementation, given there are many scenarios with async calls that would be wanted to test with this library. In the meantime, what I'm doing is to use |
I made a POC at #629. It only implements the execution of async step functions. The implementation of the fix seems so trivial that I'm not sure I want to include it in pytest-factoryboy. Anybody can make a decorator that converts async functions to sync: import asyncio, functools
def async_to_sync(fn):
"""Convert async function to sync function."""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return asyncio.run(fn(*args, **kwargs))
return wrapper and apply it to their async steps: @given("there is a cucumber", target_fixture="cucumber")
@async_to_sync
async def _():
return 42 |
Nice work @youtux !! I think it would be more like a workaround to deal easier with async functions in the steps, but there should be a better way to actually support async instead of converting async calls into sync one. I'm not familiar with the architecture of this library so I can't really say, but it looks like a support that should exist without workarounds. In the meantime you solution really helps! |
Thanks @youtux. This workaround was actually already suggested 6 years ago. Unfortunately it is not compatible with using pytest-asyncio for running async fixtures. When the test function uses an async fixture via pytest-asyncio, then an event loop is already running, but the |
I am now using the following decorator for async steps, which works well with pytest-asyncio: def async_step(step):
"""Convert an async step function to a normal one."""
signature = inspect.signature(step)
parameters = list(signature.parameters.values())
has_event_loop = any(parameter.name == "event_loop" for parameter in parameters)
if not has_event_loop:
parameters.append(
inspect.Parameter("event_loop", inspect.Parameter.POSITIONAL_OR_KEYWORD)
)
step.__signature__ = signature.replace(parameters=parameters)
@wraps(step)
def run_step(*args, **kwargs):
loop = kwargs["event_loop"] if has_event_loop else kwargs.pop("event_loop")
return loop.run_until_complete(step(*args, **kwargs))
return run_step This can be applied in the same way: @given("there is a cucumber", target_fixture="cucumber")
@async_step
async def there_is_a_cucumber():
return 42 It works like this: The |
Do you actually need all that signature machinery? Wouldn't this work as well? import asyncio, functools
def async_to_sync(fn):
"""Convert async function to sync function."""
@functools.wraps(fn)
def wrapper(event_loop, *args, **kwargs):
return event_loop.run_until_complete(fn(*args, **kwargs))
return wrapper |
See my other comment . |
Note that my last example is different from what I originally suggested. |
Sorry, yes, you're right. That simplified code should work, but does not cover the case that the original step function takes the event loop fixture as a parameter. |
Ah yes you're right |
I come back to this issue from time to time, because there's a corresponding issue in pytest-asyncio. My current understanding is that pytest-asyncio makes async step definitions hard to implement, because it assumes by default that each async test item runs in their own event loop. In pytest-bdd, however, you want to run multiple steps inside the same loop. Up until pytest-asyncio v0.21, the only way to have an asyncio event loop scope other than function-scope is to reimplement the event_loop fixture. This leads to a bunch of problems and the plan is to deprecated and remove this functionality. As a replacement for event loop fixture overrides, pytest-asyncio will provide the asyncio_event_loop mark (see pytest-dev/pytest-asyncio#620). When the mark is added to a test class or to a module, pytest-asyncio will provide an asyncio event loop with the respective scope and run all tests under the mark in that scoped loop. It should also run any async fixture inside that same loop (although this needs more testing). Here's an example from the docs: import asyncio
import pytest
import pytest_asyncio
@pytest.mark.asyncio_event_loop
class TestClassScopedLoop:
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture
async def my_fixture(self):
TestClassScopedLoop.loop = asyncio.get_running_loop()
@pytest.mark.asyncio
async def test_runs_is_same_loop_as_fixture(self, my_fixture):
assert asyncio.get_running_loop() is TestClassScopedLoop.loop I'd be happy to provide a pre-release version to play around with this feature. Alternatively, you can just pip install from Do you think this would help solve this issue? |
Makes sense to me! The only caveat is forcing having tests inside a class to allow the mark |
I imagine that pytest-bdd applies the mark under the hood via That being said, the pytest-asyncio-0.22.0 release has been yanked. It's currently unclear if markers will be the way to go. |
As a much simpler workaround, you can (at least with @fixture
async def provisioned_thing(client, thing):
await client.create(thing)
return thing
@given("a thing")
def thing(provisioned_thing):
return provisioned_thing |
is there away to use async step definitions with pytest-bdd ?
For a step definition like:
I see the warning:
pytest_bdd/scenario.py:137: RuntimeWarning: coroutine 'i_send_cucumbers' was never awaited
The text was updated successfully, but these errors were encountered: