You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -34,9 +34,9 @@ Below is the documentation example of how to use an instance of PostgreSQL, whic
34
34
35
35
## Context
36
36
37
-
The objective of this repository is to test asynchronous FastAPI endpoints using an PostgesQL with an asyncrhounous driver. To achive that, besides the testconteiner, it uses [pytest](https://docs.pytest.org/en/stable/){:target="\_blank"} and [pytest-asyncio](https://pytest-asyncio.readthedocs.io/en/latest/){:target="\_blank"}.
37
+
The objective of this repository is to test asynchronous FastAPI endpoints using PostgreSQL as a database. To achieve that, besides the `testcontainers`, it uses [`pytest`](https://docs.pytest.org/en/stable/){:target="\_blank"} and [`anyio`](https://anyio.readthedocs.io/en/stable/testing.html){:target="\_blank"}, which provides built-in support for testing applications in the form of a pytest plugin. The choice of `anyio` over `pytest-asyncio` is because FastAPI is based on Starlette, which uses AnyIO, so we don't need to install an additional package here.
38
38
39
-
The the development of the routes it uses [aiosqlite](https://aiosqlite.omnilib.dev/en/stable/){:target="\_blank"}, the async driver to SQLite.
39
+
The development of the routes uses [aiosqlite](https://aiosqlite.omnilib.dev/en/stable/){:target="\_blank"}, the async driver for SQLite.
40
40
41
41
Below are all the dependencies used to run the example.
42
42
@@ -45,12 +45,11 @@ aiosqlite>=0.20.0
45
45
asyncpg>=0.30.0
46
46
fastapi[standard]>=0.115.6
47
47
pytest>=8.3.4
48
-
pytest-asyncio>=0.24.0
49
48
sqlalchemy>=2.0.36
50
49
testcontainers>=4.8.2
51
50
```
52
51
53
-
The README contains all the steps to run it locally using [uv](https://docs.astral.sh/uv/){:target="\_blank"}.
52
+
The repository README contains all the steps to run it locally using [uv](https://docs.astral.sh/uv/){:target="\_blank"}.
54
53
55
54
Below is how the example is structured:
56
55
@@ -71,13 +70,13 @@ Below is how the example is structured:
71
70
72
71
## API routes example
73
72
74
-
This section will show the endpoints create for later tests. For this example it was created 3 simple routes simulating a ticketing sell system:
73
+
This section will show the endpoints created for later tests. For this example, three simple routes were created to simulate a ticketing sell system:
75
74
76
75
-`GET /tickets/all` - To list all the available tickets
77
76
-`POST /tickets/create` - To create a new ticket to sell
78
77
-`POST /tickets/buy` - To buy an available ticket to sell
79
78
80
-
In the database, beside the `id` field, the ticket table has: a `price` field; a boolean field `is_sold` to identify if it's sold or not; and a `sold_to` field to identify to who was sold the ticket. The `models.py` file contains these informations, using the [`SQLAlchemy`](https://www.sqlalchemy.org/){:target="\_blank"} ORM.
79
+
In the database, besides the `id` field, the ticket table has: a `price` field, a boolean field `is_sold` to identify if it's sold or not, and a `sold_to` field to identify who the ticket was sold to. The `models.py` file contains this information, using the [`SQLAlchemy`](https://www.sqlalchemy.org/){:target="\_blank"} ORM.
81
80
82
81
```py title="src/models.py" linenums="1"
83
82
from sqlalchemy.orm import Mapped, mapped_column, registry
The last file before to create the routes is the `schemas.py`, that will contain all the [pydantic](https://docs.pydantic.dev/latest/){:target="\_blank"} models.
126
+
The last file before to create the routes is the `schemas.py`, that will contain all the [Pydantic](https://docs.pydantic.dev/latest/){:target="\_blank"} models.
128
127
129
128
```py title="src/schemas.py" linenums="1"
130
129
from pydantic import BaseModel
@@ -153,9 +152,9 @@ class ListTickets(BaseModel):
153
152
tickets: list[TicketResponse]
154
153
```
155
154
156
-
The previous three files are imported in `app.py`, that contains the API routes for this example. As mentioned before, although the objective is to test the endpoints in a PostgreSQL database, the development of the API is done by using SQLite to avoid the necessity to have an instance of PostgreSQL running all the time.
155
+
The previous three files are imported in `app.py`, which contains the API routes for this example. As mentioned before, although the objective is to test the endpoints in a PostgreSQL database, the development of the API is done using SQLite to avoid the necessity of having an instance of PostgreSQL running all the time.
157
156
158
-
To keep it simple and avoid using database migrations, the database creation is handled using the [lifespan events](https://fastapi.tiangolo.com/advanced/events/){:target="\_blank"}. Here, it will just guarantee that every time we run the application, a database will be created in case it's not yet.
157
+
To keep it simple and avoid using database migrations, the database creation is handled using the [lifespan events](https://fastapi.tiangolo.com/advanced/events/){:target="\_blank"}. Here, it will just guarantee that every time we run the application, a database will be created in case it doesn't exist yet.
To use PostgeSQL in the tests, the testcontainer will be setup in the `conftest.py`, as also the database session and the client to be used to test our endpoints.
273
+
To use PostgreSQL in the tests, the testcontainer will be set up in `conftest.py`, along with the database session and the client required to test the endpoints.
275
274
276
-
Below is a asimple schem of how it will work for each tests, where each block will represent a different function.
275
+
Below is a simple diagram illustrating how it works for each test, where each block represents a different function.
277
276
278
277
```mermaid
279
278
flowchart LR
@@ -292,6 +291,7 @@ flowchart LR
292
291
293
292
%% Arrows from async_client to test blocks
294
293
async_client --> test
294
+
async_session --> test
295
295
296
296
%% Style the nodes with rounded corners
297
297
classDef fixtureStyle rx:10, ry:10;
@@ -300,14 +300,14 @@ flowchart LR
300
300
class postgres_container,async_session,async_client,test fixtureStyle;
301
301
```
302
302
303
-
As we are working with the async fistures, all the funtion will have the decorator `@pytest_asyncio.fixture` assigned.
303
+
The `postgres_container` will be passed to `async_session`, which will be used in both `async_client` and directly in the tests in some cases where we need to transact directly with the database.
304
304
305
305
## Creating the test fixtures
306
306
307
-
The `postgres_container`function will create the PostgreSQL intsnace and provide the database URL to be connected in the `async_session` funtion.
307
+
The first fixture inserted in `conftest.py` is the `anyio_backend`, highlighted in the code below. This function will be used in `postgres_container` and marked for the AnyIO pytest plugin, as well as setting `asyncio` as the backend to run the tests. This function was not included in the previous diagram because it is an AnyIO specification. You can check more details about it [here](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on).
@@ -321,16 +321,26 @@ from src.database import get_session
321
321
from src.models import table_register
322
322
323
323
324
-
@pytest_asyncio.fixture
325
-
defpostgres_container():
324
+
@pytest.fixture
325
+
defanyio_backend():
326
+
return'asyncio'
327
+
```
328
+
329
+
Now, in the `postgres_container`, the `anyio_backend` is passed, and all the tests that use the `postgres_container` as a fixture at some level will be marked to run asynchronously.
330
+
331
+
Below is the `postgres_container` function, which will be responsible for creating the `PostgresContainer` instance from `testcontainers`. The `asyncpg` is passed as an argument in the class to specify that this will be the driver used.
332
+
333
+
```py title="tests/conftest.py" linenums="20"
334
+
@pytest.fixture
335
+
defpostgres_container(anyio_backend):
326
336
with PostgresContainer('postgres:16', driver='asyncpg') as postgres:
327
337
yield postgres
328
338
```
329
339
330
-
In`async_session`, it takes the url connection from the PostgresContainer object, and use it to cretae the tables inside the Database, as also the session that will establishe all conversations with the POstgreSQl instance created. The function will persist this session to be used in `async_client`, and them restarte the database to the next test.
340
+
The`async_session`takes the connection URL from the `PostgresContainer` object returned by the `postgres_container` function and uses it to create the tables inside the database, as well as the session that will establish all interactions with the PostgreSQL instance created. The function will return and persist a session to be used, and then restore the database for the next test by deleting the tables.
The `async_client` functionwill create the [`AsyncClient`](https://fastapi.tiangolo.com/advanced/async-tests/), directly importted from [`HTTPX`](https://www.python-httpx.org/), and provide it to get reqeusts from our endpoints. Here, the session provided by `async_session`wil overrides the session originally used in our app as dependencie injection while the client is being used.
365
+
The last fixture is the `async_client` function, which will create the [`AsyncClient`](https://fastapi.tiangolo.com/advanced/async-tests/), directly imported from [HTTPX](https://www.python-httpx.org/), and provide it to make requests to our endpoints. Here, the session provided by `async_session`will override the session originally used in our app as dependency injection while the client is being used.
With all the test fixtures created, it's now possible to write and run the tests.
384
+
385
+
Below are the test examples for the `GET /tickets/all`. The first one inserts 3 records in the table using the `async_session` and then asserts if the response has a 200 status and the list returned has a length of 3. The second one tests the case where there are no records yet in the database, as the response must return a 200 status and an empty list.
0 commit comments