Skip to content

Commit d962234

Browse files
duganchentiangolo
andauthored
📝 Add an example of setting up a test database (fastapi#1144)
* Add an example of setting up a test database. * 📝 Add/update docs for testing a DB with dependency overrides * 🔧 Update test script, remove line removing test file as it is removed during testing * ✅ Update testing coverage pragma Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
1 parent fd99dfc commit d962234

File tree

11 files changed

+161
-21
lines changed

11 files changed

+161
-21
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Testing a Database
2+
3+
You can use the same dependency overrides from [Testing Dependencies with Overrides](testing-dependencies.md){.internal-link target=_blank} to alter a database for testing.
4+
5+
You could want to set up a different database for testing, rollback the data after the tests, pre-fill it with some testing data, etc.
6+
7+
The main idea is exactly the same you saw in that previous chapter.
8+
9+
## Add tests for the SQL app
10+
11+
Let's update the example from [SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank} to use a testing database.
12+
13+
All the app code is the same, you can go back to that chapter check how it was.
14+
15+
The only changes here are in the new testing file.
16+
17+
Your normal dependency `get_db()` would return a database session.
18+
19+
In the test, you could use a dependency override to return your *custom* database session instead of the one that would be used normally.
20+
21+
In this example we'll create a temporary database only for the tests.
22+
23+
## File structure
24+
25+
We create a new file at `sql_app/tests/test_sql_app.py`.
26+
27+
So the new file structure looks like:
28+
29+
``` hl_lines="9 10 11"
30+
.
31+
└── sql_app
32+
├── __init__.py
33+
├── crud.py
34+
├── database.py
35+
├── main.py
36+
├── models.py
37+
├── schemas.py
38+
└── tests
39+
├── __init__.py
40+
└── test_sql_app.py
41+
```
42+
43+
## Create the new database session
44+
45+
First, we create a new database session with the new database.
46+
47+
For the tests we'll use a file `test.db` instead of `sql_app.db`.
48+
49+
But the rest of the session code is more or less the same, we just copy it.
50+
51+
```Python hl_lines="8 9 10 11 12 13"
52+
{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!}
53+
```
54+
55+
!!! tip
56+
You could reduce duplication in that code by putting it in a function and using it from both `database.py` and `tests/test_sql_app.py`.
57+
58+
For simplicity and to focus on the specific testing code, we are just copying it.
59+
60+
## Create the database
61+
62+
Because now we are going to use a new database in a new file, we need to make sure we create the database with:
63+
64+
```Python
65+
Base.metadata.create_all(bind=engine)
66+
```
67+
68+
That is normally called in `main.py`, but the line in `main.py` uses the database file `sql_app.db`, and we need to make sure we create `test.db` for the tests.
69+
70+
So we add that line here, with the new file.
71+
72+
```Python hl_lines="16"
73+
{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!}
74+
```
75+
76+
## Dependency override
77+
78+
Now we create the dependency override and add it to the overrides for our app.
79+
80+
```Python hl_lines="19 20 21 22 23 24 27"
81+
{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!}
82+
```
83+
84+
!!! tip
85+
The code for `override_get_db()` is almost exactly the same as for `get_db()`, but in `override_get_db()` we use the `TestingSessionLocal` for the testing database instead.
86+
87+
## Test the app
88+
89+
Then we can just test the app as normally.
90+
91+
```Python hl_lines="32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47"
92+
{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!}
93+
```
94+
95+
And all the modifications we made in the database during the tests will be in the `test.db` database instead of the main `sql_app.db`.

docs/en/docs/advanced/testing-dependencies.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,6 @@ You probably want to test the external provider once, but not necessarily call i
2020

2121
In this case, you can override the dependency that calls that provider, and use a custom dependency that returns a mock user, only for your tests.
2222

23-
### Use case: testing database
24-
25-
Other example could be that you are using a specific database only for testing.
26-
27-
Your normal dependency would return a database session.
28-
29-
But then, after each test, you could want to rollback all the operations or remove data.
30-
31-
Or you could want to alter the data before the tests run, etc.
32-
33-
In this case, you could use a dependency override to return your *custom* database session instead of the one that would be used normally.
34-
3523
### Use the `app.dependency_overrides` attribute
3624

3725
For these cases, your **FastAPI** application has an attribute `app.dependency_overrides`, it is a simple `dict`.

docs/en/docs/tutorial/sql-databases.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ Let's refer to the file `sql_app/database.py`.
9898

9999
In this example, we are "connecting" to a SQLite database (opening a file with the SQLite database).
100100

101-
The file will be located at the same directory in the file `test.db`.
101+
The file will be located at the same directory in the file `sql_app.db`.
102102

103-
That's why the last part is `./test.db`.
103+
That's why the last part is `./sql_app.db`.
104104

105105
If you were using a **PostgreSQL** database instead, you would just have to uncomment the line:
106106

docs/en/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ nav:
9999
- advanced/testing-websockets.md
100100
- advanced/testing-events.md
101101
- advanced/testing-dependencies.md
102+
- advanced/testing-database.md
102103
- advanced/settings.md
103104
- advanced/extending-openapi.md
104105
- advanced/openapi-callbacks.md

docs_src/sql_databases/sql_app/database.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from sqlalchemy.ext.declarative import declarative_base
33
from sqlalchemy.orm import sessionmaker
44

5-
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
5+
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
66
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"
77

88
engine = create_engine(

docs_src/sql_databases/sql_app/tests/__init__.py

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from fastapi.testclient import TestClient
2+
from sqlalchemy import create_engine
3+
from sqlalchemy.orm import sessionmaker
4+
5+
from ..database import Base
6+
from ..main import app, get_db
7+
8+
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
9+
10+
engine = create_engine(
11+
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
12+
)
13+
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
14+
15+
16+
Base.metadata.create_all(bind=engine)
17+
18+
19+
def override_get_db():
20+
try:
21+
db = TestingSessionLocal()
22+
yield db
23+
finally:
24+
db.close()
25+
26+
27+
app.dependency_overrides[get_db] = override_get_db
28+
29+
client = TestClient(app)
30+
31+
32+
def test_create_user():
33+
response = client.post(
34+
"/users/",
35+
json={"email": "deadpool@example.com", "password": "chimichangas4life"},
36+
)
37+
assert response.status_code == 200
38+
data = response.json()
39+
assert data["email"] == "deadpool@example.com"
40+
assert "id" in data
41+
user_id = data["id"]
42+
43+
response = client.get(f"/users/{user_id}")
44+
assert response.status_code == 200
45+
data = response.json()
46+
assert data["email"] == "deadpool@example.com"
47+
assert data["id"] == user_id

scripts/test.sh

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@
33
set -e
44
set -x
55

6-
# Remove temporary DB
7-
if [ -f ./test.db ]; then
8-
rm ./test.db
9-
fi
106
bash ./scripts/lint.sh
117
# Check README.md is up to date
128
diff --brief docs/en/docs/index.md README.md

tests/test_tutorial/test_sql_databases/test_sql_databases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def client():
286286
# Import while creating the client to create the DB after starting the test session
287287
from sql_databases.sql_app.main import app
288288

289-
test_db = Path("./test.db")
289+
test_db = Path("./sql_app.db")
290290
with TestClient(app) as c:
291291
yield c
292292
test_db.unlink()

tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def client():
286286
# Import while creating the client to create the DB after starting the test session
287287
from sql_databases.sql_app.alt_main import app
288288

289-
test_db = Path("./test.db")
289+
test_db = Path("./sql_app.db")
290290
with TestClient(app) as c:
291291
yield c
292292
test_db.unlink()

0 commit comments

Comments
 (0)