This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
AsyncClient ignores the startup and shutdown events #2003
Comments
If a minimal example is provided, others can check. 🎉 |
@Kludex pls take a look one more time |
@zhukovgreen Which will look like this:
|
@ArcLightSlavik It's the recommended solution on our documentation. |
Ah, didn't see 🙂 |
More info in: encode/httpx#350 |
@ArcLightSlavik thank you. I believe async test client should support app lifecycle. |
It's all discussed in encode/httpx#350 and related issues, but I'm gonna leave here a short summary. As described in encode/httpx#1441, app lifecycle managemenent won't be added to HTTPX's Suggested solution for original problem is to use So import pytest
from asgi_lifespan import LifespanManager
from httpx import AsyncClient
from main import app
@pytest.fixture()
async def client():
async with AsyncClient(app=app, base_url="http://test") as client, LifespanManager(app):
yield client
@pytest.mark.asyncio
async def test_read_main(client):
response = await client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
assert 'X-Test-Header' in response.headers I think it should be at least mentioned in "Testing Events: startup - shutdown" section of FastAPI docs. |
I agree with @blazewicz . Does someone want to give it a try? |
@blazewicz Do you happen to have an example of how to access the application instance using For example, this is my startup event def startup_handler(app: FastAPI) -> Callable:
def startup() -> None:
logger.info("Running startup handler.")
app.state.model = Model()
return startup
app.add_event_handler("startup", startup_handler(app)) And I am trying to test like so but @pytest.mark.asyncio
class TestEvents:
async def test_startup(self, test_client: AsyncClient) -> None:
res = await test_client.get("/health")
assert res.status_code == 200
assert isinstance(test_client.app.state.model, Model) |
Since AsyncClient is the documented async test procedure, and it is missing this commonly-used function, would it be possible to document this and maybe suggest a preferred work-around? Thanks. |
Now that my tests are running, I will suggest that the so-called hack suggested by the original poster may in fact be desired best practice. This is because, once I got the first test running under Like all of Here is how I refactored my code: # This code block is my refactored main.py
def app_factory():
myapp = FastAPI()
myapp.include_router(module_a.router)
myapp.include_router(module_b.router)
return myapp
async def app_startup(app):
pass # startup goes here
async def app_shutdown(app):
pass # shutdown goes here
app = app_factory()
@app.on_event("startup")
async def startup():
await app_startup(app)
@app.on_event("shutdown")
async def shutdown():
await app_shutdown(app) Then, in my tests, I have a fixture that creates a brand new FastAPI instance for each test function so that nothing gets confused by all the new event loops being created and destroyed under the covers by pytest-asyncio. # this code block goes in conftest.py and simultaneously solves both the startup/shutdown doesn't run problem
# as well as my issues with running parametrized tests
from main import app_factory, app_startup, app_shutdown
@pytest.fixture
async def app():
app = app_factory()
await app_startup(app)
yield app
await app_shutdown(app) And here's a stub for a test: @pytest.mark.parametrize("param_a,param_b", [[1,2],[3,4],[5,6]])
@pytest.mark.asyncio
async def test_module_a(
app,
param_a,
param_b
):
async with AsyncClient(app=app, base_url="http://test") as ac:
params = {}
params["param_a"] = param_a
params["param_b"] = param_b
response = await ac.get("/module_a/some_api", params=params)
assert response.status_code == 200 Edit to add: If you don't mind having your event_loop stick around between tests, this issue shows how to prevent pytest-asyncio from creating new loops during the test: #2006 Edited to patch up the pseudo-code a bit more. |
Improve docs as suggested in tiangolo#2003.
I just created #4167 to begin addressing this in the docs. Anyone wanna take a look and give me some feedback on how I could improve the warning I added? I wrote the warning assuming that the LifespanManager context manager is the recommended solution. |
Rather than just a warning, make it a documentation section with code snippets. That would make it much easier to give it a try. |
For me, the following construction works:
|
The problem with that is your test requires more intimate knowledge of where the critical startup shutdown code is. By isolating app setup in a fixture, I don't have to put those "awaits" in each test when I create the AsyncClient. Furthermore, by making the fixture call the app_startup and app_shutdown functions defined in my application, the fixture itself is further isolated from the details of the implementation. |
Yes, the code is isolated in a fixture :)
|
Inside a fixture makes more sense! But does your solution only work with scope="session"? If I can use it for smaller scopes as well, then it seems to be a simpler solution than what I did. |
The scope doesn't matter... I prefer bigger scopes because it makes tests faster (in this case using |
Was getting the error
Depending on your options with You don't need to worry about this if you have set |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
This is very close to #1072, but for AsyncClient.
The problem is that AsyncClient ignores the startup and shutdown events.
To Reproduce
Steps to reproduce the behavior with a minimum self-contained file.
Replace each part with your own scenario:
Run pytest -s -v test_startup.py
You will see an AssertionError for the X-Test-Header not being there
The test should pass
So far I am using a hack to make it work:
but probably some solution or a note in the docs would compliment the project
The text was updated successfully, but these errors were encountered: