]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
📝 Add documentation about async tests (pytest-asyncio and httpx) (#1619)
authorFelix Böhm <felix@felixboehm.dev>
Sat, 8 Aug 2020 18:01:18 +0000 (20:01 +0200)
committerGitHub <noreply@github.com>
Sat, 8 Aug 2020 18:01:18 +0000 (20:01 +0200)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
docs/en/docs/advanced/async-tests.md [new file with mode: 0644]
docs/en/docs/tutorial/testing.md
docs/en/mkdocs.yml
docs_src/async_tests/__init__.py [new file with mode: 0644]
docs_src/async_tests/main.py [new file with mode: 0644]
docs_src/async_tests/test_main.py [new file with mode: 0644]
pyproject.toml
tests/test_tutorial/test_async_tests/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_async_tests/test_main.py [new file with mode: 0644]

diff --git a/docs/en/docs/advanced/async-tests.md b/docs/en/docs/advanced/async-tests.md
new file mode 100644 (file)
index 0000000..9e7cc70
--- /dev/null
@@ -0,0 +1,100 @@
+# Async Tests
+
+You have already seen how to test your **FastAPI** applications using the provided `TestClient`, but with it, you can't test or run any other `async` function in your (synchronous) pytest functions.
+
+Being able to use asynchronous functions in your tests could be useful, for example, when you're querying your database asynchronously. Imagine you want to test sending requests to your FastAPI application and then verify that your backend successfully wrote the correct data in the database, while using an async database library.
+
+Let's look at how we can make that work.
+
+## pytest-asyncio
+
+If we want to call asynchronous functions in our tests, our test functions have to be asynchronous. Pytest provides a neat library for this, called `pytest-asyncio`, that allows us to specify that some test functions are to be called asynchronously.
+
+You can install it via:
+
+<div class="termy">
+
+```console
+$ pip install pytest-asyncio
+
+---> 100%
+```
+
+</div>
+
+## HTTPX
+
+Even if your **FastAPI** application uses normal `def` functions instead of `async def`, it is still an `async` application underneath.
+
+The `TestClient` does some magic inside to call the asynchronous FastAPI application in your normal `def` test functions, using standard pytest. But that magic doesn't work anymore when we're using it inside asynchronous functions. By running our tests asynchronously, we can no longer use the `TestClient` inside our test functions.
+
+Luckily there's a nice alternative, called <a href="https://www.python-httpx.org/" class="external-link" target="_blank">HTTPX</a>.
+
+HTTPX is an HTTP client for Python 3 that allows us to query our FastAPI application similarly to how we did it with the `TestClient`.
+
+If you're familiar with the <a href="https://requests.readthedocs.io/en/master/" class="external-link" target="_blank">Requests</a> library, you'll find that the API of HTTPX is almost identical.
+
+The important difference for us is that with HTTPX we are not limited to synchronous, but can also make asynchronous requests.
+
+## Example
+
+For a simple example, let's consider the following `main.py` module:
+
+```Python
+{!../../../docs_src/async_tests/main.py!}
+```
+
+The `test_main.py` module that contains the tests for `main.py` could look like this now:
+
+```Python
+{!../../../docs_src/async_tests/test_main.py!}
+```
+
+## Run it
+
+You can run your tests as usual via:
+
+<div class="termy">
+
+```console
+$ pytest
+
+---> 100%
+```
+
+</div>
+
+## In Detail
+
+The marker `@pytest.mark.asyncio` tells pytest that this test function should be called asynchronously:
+
+```Python hl_lines="7"
+{!../../../docs_src/async_tests/test_main.py!}
+```
+
+!!! tip
+    Note that the test function is now `async def` instead of just `def` as before when using the `TestClient`.
+
+Then we can create an `AsyncClient` with the app, and send async requests to it, using `await`.
+
+```Python hl_lines="9 10"
+{!../../../docs_src/async_tests/test_main.py!}
+```
+
+This is the equivalent to:
+
+```Python
+response = client.get('/')
+```
+
+that we used to make our requests with the `TestClient`.
+
+!!! tip
+    Note that we're using async/await with the new `AsyncClient` - the request is asynchronous.
+
+## Other Asynchronous Function Calls
+
+As the testing function is now asynchronous, you can now also call (and `await`) other `async` functions apart from sending requests to your FastAPI application in your tests, exactly as you would call them anywhere else in your code.
+
+!!! tip
+    If you encounter a `RuntimeError: Task attached to a different loop` when integrating asynchronous function calls in your tests (e.g. when using <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB's MotorClient</a>) check out <a href="https://github.com/pytest-dev/pytest-asyncio/issues/38#issuecomment-264418154" class="external-link" target="_blank">this issue</a> in the pytest-asyncio repository.
index 9296a20b7b2b0fc63768e5cfaa1064619fa21891..e517ae78174cc984e163202416b86111465dfed6 100644 (file)
@@ -34,6 +34,9 @@ Write simple `assert` statements with the standard Python expressions that you n
 
     **FastAPI** provides the same `starlette.testclient` as `fastapi.testclient` just as a convenience for you, the developer. But it comes directly from Starlette.
 
+!!! tip
+    If you want to call `async` functions in your tests apart from sending requests to your FastAPI application (e.g. asynchronous database functions), have a look at the [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} in the advanced tutorial.
+
 ## Separating tests
 
 In a real application, you probably would have your tests in a different file.
index 419d83b9e1a02008dd489dd88956ef50b678f95a..e72e874082fb54fa9c86c9efe1946ad4499f2eaf 100644 (file)
@@ -111,6 +111,7 @@ nav:
   - advanced/testing-events.md
   - advanced/testing-dependencies.md
   - advanced/testing-database.md
+  - advanced/async-tests.md
   - advanced/settings.md
   - advanced/conditional-openapi.md
   - advanced/extending-openapi.md
diff --git a/docs_src/async_tests/__init__.py b/docs_src/async_tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/async_tests/main.py b/docs_src/async_tests/main.py
new file mode 100644 (file)
index 0000000..9594f85
--- /dev/null
@@ -0,0 +1,8 @@
+from fastapi import FastAPI
+
+app = FastAPI()
+
+
+@app.get("/")
+async def root():
+    return {"message": "Tomato"}
diff --git a/docs_src/async_tests/test_main.py b/docs_src/async_tests/test_main.py
new file mode 100644 (file)
index 0000000..c141d86
--- /dev/null
@@ -0,0 +1,12 @@
+import pytest
+from httpx import AsyncClient
+
+from .main import app
+
+
+@pytest.mark.asyncio
+async def test_root():
+    async with AsyncClient(app=app, base_url="http://test") as ac:
+        response = await ac.get("/")
+    assert response.status_code == 200
+    assert response.json() == {"message": "Tomato"}
index fcb3f52cedd3c1fc8234211483f2df3dff3d5c4f..3c10b5925b3c92712d02466efc9d206eda0173d8 100644 (file)
@@ -45,10 +45,12 @@ Documentation = "https://fastapi.tiangolo.com/"
 test = [
     "pytest ==5.4.3",
     "pytest-cov ==2.10.0",
+    "pytest-asyncio >=0.14.0,<0.15.0",
     "mypy ==0.782",
     "black ==19.10b0",
     "isort >=5.0.6,<6.0.0",
     "requests >=2.24.0,<3.0.0",
+    "httpx >=0.14.0,<0.15.0",
     "email_validator >=1.1.1,<2.0.0",
     "sqlalchemy >=1.3.18,<2.0.0",
     "peewee >=3.13.3,<4.0.0",
diff --git a/tests/test_tutorial/test_async_tests/__init__.py b/tests/test_tutorial/test_async_tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_async_tests/test_main.py b/tests/test_tutorial/test_async_tests/test_main.py
new file mode 100644 (file)
index 0000000..8104c90
--- /dev/null
@@ -0,0 +1,8 @@
+import pytest
+
+from docs_src.async_tests.test_main import test_root
+
+
+@pytest.mark.asyncio
+async def test_async_testing():
+    await test_root()