Skip to content

Commit 06a0a33

Browse files
authored
Update tests structure. (#68)
* Update tests structure. * Unit tests use the test database * Add function for creating database engine and session.
1 parent 4954117 commit 06a0a33

File tree

16 files changed

+181
-154
lines changed

16 files changed

+181
-154
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,21 @@ Execute the `backend/app/init_test_data.py` file
8484

8585
Perform tests via pytest
8686

87-
**Tip**: Before the test starts, please execute init the test data first, also, the fastapi service needs to be started
87+
1. Create a database `fba_test`, choose utf8mb4 encode
8888

89-
1. First, go to the app directory
89+
2. First, go to the app directory
9090

9191
```shell
9292
cd backend/app/
9393
```
94+
95+
3. Init the test data
96+
97+
```shell
98+
python tests/init_test_data.py
99+
```
94100

95-
2. Execute the test command
101+
4. Execute the test command
96102

97103
```shell
98104
pytest -vs --disable-warnings

README.zh-CN.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,21 @@ git clone https://github.com/wu-clan/fastapi_best_architecture.git
7777

7878
通过 pytest 进行测试
7979

80-
**提示**: 在测试开始前,请先执行初始化测试数据,同时,需要启动 fastapi 服务。
80+
1. 创建一个数据库`fba_test`,选择 utf8mb4 编码
8181

82-
1. 首先,进入app目录
82+
2. 首先,进入app目录
8383

8484
```shell
8585
cd backend/app/
8686
```
87+
88+
3. 初始化测试数据
89+
90+
```shell
91+
python tests/init_test_data.py
92+
```
8793

88-
2. 执行测试命令
94+
4. 执行测试命令
8995

9096
```shell
9197
pytest -vs --disable-warnings

backend/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-

backend/app/api/v1/auth/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55

66
router = APIRouter(prefix='/auth', tags=['认证'])
77

8-
router.include_router(auth_router, prefix='/users')
8+
router.include_router(auth_router)

backend/app/core/conf.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ class Settings(BaseSettings):
3232
TOKEN_WHITE_LIST: list[str] # 白名单用户ID,可多点登录
3333

3434
# FastAPI
35+
API_V1_STR: str = '/v1'
3536
TITLE: str = 'FastAPI'
3637
VERSION: str = '0.0.1'
3738
DESCRIPTION: str = 'FastAPI Best Architecture'
38-
DOCS_URL: str | None = '/v1/docs'
39-
REDOCS_URL: str | None = '/v1/redocs'
40-
OPENAPI_URL: str | None = '/v1/openapi'
39+
DOCS_URL: str | None = f'{API_V1_STR}/docs'
40+
REDOCS_URL: str | None = f'{API_V1_STR}/redocs'
41+
OPENAPI_URL: str | None = f'{API_V1_STR}/openapi'
4142

4243
@root_validator
4344
def validator_api_url(cls, values):
@@ -54,7 +55,7 @@ def validator_api_url(cls, values):
5455
STATIC_FILES: bool = False
5556

5657
# MySQL
57-
DB_ECHO: bool = True
58+
DB_ECHO: bool = False
5859
DB_DATABASE: str = 'fba'
5960
DB_CHARSET: str = 'utf8mb4'
6061

@@ -72,7 +73,7 @@ def validator_api_url(cls, values):
7273
# Token
7374
TOKEN_ALGORITHM: str = 'HS256' # 算法
7475
TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1 # 过期时间,单位:秒
75-
TOKEN_URL_SWAGGER: str = '/v1/auth/users/swagger_login'
76+
TOKEN_URL_SWAGGER: str = f'{API_V1_STR}/auth/swagger_login'
7677
TOKEN_REDIS_PREFIX: str = 'fba_token'
7778

7879
# Log
@@ -86,10 +87,10 @@ def validator_api_url(cls, values):
8687
# Casbin
8788
CASBIN_RBAC_MODEL_NAME: str = 'rbac_model.conf'
8889
CASBIN_EXCLUDE: list[dict[str, str], dict[str, str]] = [
89-
{'method': 'POST', 'path': '/api/v1/auth/users/swagger_login'},
90-
{'method': 'POST', 'path': '/api/v1/auth/users/login'},
91-
{'method': 'POST', 'path': '/api/v1/auth/users/register'},
92-
{'method': 'POST', 'path': '/api/v1/auth/users/password/reset'},
90+
{'method': 'POST', 'path': '/api/v1/auth/swagger_login'},
91+
{'method': 'POST', 'path': '/api/v1/auth/login'},
92+
{'method': 'POST', 'path': '/api/v1/auth/register'},
93+
{'method': 'POST', 'path': '/api/v1/auth/password/reset'},
9394
]
9495

9596
class Config:

backend/app/database/db_mysql.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44

55
from fastapi import Depends
6+
from sqlalchemy import URL
67
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
78
from typing_extensions import Annotated
89

@@ -14,20 +15,26 @@
1415
说明:SqlAlchemy
1516
"""
1617

18+
19+
def create_engine_and_session(url: str | URL):
20+
try:
21+
# 数据库引擎
22+
engine = create_async_engine(url, echo=settings.DB_ECHO, future=True, pool_pre_ping=True)
23+
# log.success('数据库连接成功')
24+
except Exception as e:
25+
log.error('❌ 数据库链接失败 {}', e)
26+
sys.exit()
27+
else:
28+
db_session = async_sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
29+
return engine, db_session
30+
31+
1732
SQLALCHEMY_DATABASE_URL = (
1833
f'mysql+asyncmy://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:'
1934
f'{settings.DB_PORT}/{settings.DB_DATABASE}?charset={settings.DB_CHARSET}'
2035
)
2136

22-
try:
23-
# 数据库引擎
24-
async_engine = create_async_engine(SQLALCHEMY_DATABASE_URL, echo=settings.DB_ECHO, future=True, pool_pre_ping=True)
25-
# log.success('数据库连接成功')
26-
except Exception as e:
27-
log.error('❌ 数据库链接失败 {}', e)
28-
sys.exit()
29-
else:
30-
async_db_session = async_sessionmaker(bind=async_engine, autoflush=False, expire_on_commit=False)
37+
async_engine, async_db_session = create_engine_and_session(SQLALCHEMY_DATABASE_URL)
3138

3239

3340
async def get_db() -> AsyncSession:

backend/app/init_test_data.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,26 @@
1414
class InitTestData:
1515
"""初始化测试数据"""
1616

17-
def __init__(self):
17+
def __init__(self, session):
1818
self.fake = Faker('zh_CN')
19+
self.session = session
1920

20-
@staticmethod
21-
async def create_dept():
21+
async def create_dept(self):
2222
"""自动创建部门"""
23-
async with async_db_session.begin() as db:
23+
async with self.session.begin() as db:
2424
department_obj = Dept(name='test', create_user=1)
2525
db.add(department_obj)
2626
log.info('部门 test 创建成功')
2727

28-
@staticmethod
29-
async def create_role():
28+
async def create_role(self):
3029
"""自动创建角色"""
31-
async with async_db_session.begin() as db:
30+
async with self.session.begin() as db:
3231
role_obj = Role(name='test', create_user=1)
3332
role_obj.menus.append(Menu(name='test', create_user=1))
3433
db.add(role_obj)
3534
log.info('角色 test 创建成功')
3635

37-
@staticmethod
38-
async def create_test_user():
36+
async def create_test_user(self):
3937
"""创建测试用户"""
4038
username = 'test'
4139
password = 'test'
@@ -48,13 +46,12 @@ async def create_test_user():
4846
is_superuser=True,
4947
dept_id=1,
5048
)
51-
async with async_db_session.begin() as db:
49+
async with self.session.begin() as db:
5250
user_obj.roles.append(await db.get(Role, 1))
5351
db.add(user_obj)
5452
log.info(f'测试用户创建成功,账号:{username},密码:{password}')
5553

56-
@staticmethod
57-
async def create_superuser_by_yourself():
54+
async def create_superuser_by_yourself(self):
5855
"""手动创建管理员账户"""
5956
log.info('开始创建自定义管理员用户')
6057
print('请输入用户名:')
@@ -78,7 +75,7 @@ async def create_superuser_by_yourself():
7875
is_superuser=True,
7976
dept_id=1,
8077
)
81-
async with async_db_session.begin() as db:
78+
async with self.session.begin() as db:
8279
user_obj.roles.append(await db.get(Role, 1))
8380
db.add(user_obj)
8481
log.info(f'自定义管理员用户创建成功,账号:{username},密码:{password}')
@@ -96,7 +93,7 @@ async def fake_user(self):
9693
is_superuser=False,
9794
dept_id=1,
9895
)
99-
async with async_db_session.begin() as db:
96+
async with self.session.begin() as db:
10097
user_obj.roles.append(await db.get(Role, 1))
10198
db.add(user_obj)
10299
log.info(f'普通用户创建成功,账号:{username},密码:{password}')
@@ -115,7 +112,7 @@ async def fake_no_active_user(self):
115112
is_superuser=False,
116113
dept_id=1,
117114
)
118-
async with async_db_session.begin() as db:
115+
async with self.session.begin() as db:
119116
user_obj.roles.append(await db.get(Role, 1))
120117
db.add(user_obj)
121118
log.info(f'普通锁定用户创建成功,账号:{username},密码:{password}')
@@ -133,7 +130,7 @@ async def fake_superuser(self):
133130
is_superuser=True,
134131
dept_id=1,
135132
)
136-
async with async_db_session.begin() as db:
133+
async with self.session.begin() as db:
137134
user_obj.roles.append(await db.get(Role, 1))
138135
db.add(user_obj)
139136
log.info(f'管理员用户创建成功,账号:{username},密码:{password}')
@@ -152,7 +149,7 @@ async def fake_no_active_superuser(self):
152149
is_superuser=True,
153150
dept_id=1,
154151
)
155-
async with async_db_session.begin() as db:
152+
async with self.session.begin() as db:
156153
user_obj.roles.append(await db.get(Role, 1))
157154
db.add(user_obj)
158155
log.info(f'管理员锁定用户创建成功,账号:{username},密码:{password}')
@@ -172,6 +169,6 @@ async def init_data(self):
172169

173170

174171
if __name__ == '__main__':
175-
init = InitTestData()
172+
init = InitTestData(session=async_db_session)
176173
loop = asyncio.get_event_loop()
177174
loop.run_until_complete(init.init_data())

backend/app/tests/api_v1/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-

backend/app/tests/api_v1/test_auth.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from starlette.testclient import TestClient
4+
5+
from backend.app.core.conf import settings
6+
7+
8+
def test_login(client: TestClient) -> None:
9+
data = {
10+
'username': 'test',
11+
'password': 'test',
12+
}
13+
response = client.post(f'{settings.API_V1_STR}/auth/login', json=data)
14+
assert response.status_code == 200
15+
assert response.json()['data']['access_token_type'] == 'Bearer'
16+
17+
18+
def test_logout(client: TestClient, token_headers: dict[str, str]) -> None:
19+
response = client.post(f'{settings.API_V1_STR}/auth/logout', headers=token_headers)
20+
assert response.status_code == 200
21+
assert response.json()['code'] == 200

backend/app/tests/conftest.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,27 @@
22
# -*- coding: utf-8 -*-
33
import sys
44

5+
sys.path.append('../../')
6+
57
import pytest
6-
from httpx import AsyncClient
8+
from typing import Generator, Dict
79

8-
sys.path.append('../../')
10+
from starlette.testclient import TestClient
11+
12+
from backend.app.main import app
13+
from backend.app.tests.utils.get_headers import get_token_headers
14+
from backend.app.database.db_mysql import get_db
15+
from backend.app.tests.utils.db_mysql import override_get_db
916

10-
from backend.app.common.redis import redis_client # noqa: E402
11-
from backend.app.core.conf import settings # noqa: E402
17+
app.dependency_overrides[get_db] = override_get_db
1218

1319

14-
@pytest.fixture(scope='session')
15-
def anyio_backend():
16-
return 'asyncio'
20+
@pytest.fixture(scope='module')
21+
def client() -> Generator:
22+
with TestClient(app) as c:
23+
yield c
1724

1825

19-
@pytest.fixture(scope='package', autouse=True)
20-
async def function_fixture(anyio_backend):
21-
auth_data = {
22-
'url': f'http://{settings.UVICORN_HOST}:{settings.UVICORN_PORT}/v1/auth/users/login',
23-
'headers': {'accept': 'application/json', 'Content-Type': 'application/json'},
24-
'json': {'username': 'test', 'password': 'test'},
25-
}
26-
async with AsyncClient() as client:
27-
response = await client.post(**auth_data)
28-
token = response.json()['data']['access_token']
29-
test_token = await redis_client.get('test_token')
30-
if not test_token:
31-
await redis_client.set('test_token', token, ex=86400)
26+
@pytest.fixture(scope='module')
27+
def token_headers(client: TestClient) -> Dict[str, str]:
28+
return get_token_headers(client=client, username='test', password='test')

0 commit comments

Comments
 (0)