Files
disknext/tests/TESTING_GUIDE.md
于小丘 f93cb3eedb Add unit tests for models and services
- Implemented unit tests for Object model including folder and file creation, properties, and path retrieval.
- Added unit tests for Setting model covering creation, unique constraints, and type enumeration.
- Created unit tests for User model focusing on user creation, uniqueness, and group relationships.
- Developed unit tests for Login service to validate login functionality, including 2FA and token generation.
- Added utility tests for JWT creation and verification, ensuring token integrity and expiration handling.
- Implemented password utility tests for password generation, hashing, and TOTP verification.
2025-12-19 19:48:05 +08:00

666 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DiskNext Server 测试基础设施使用指南
本文档介绍如何使用新的测试基础设施进行单元测试和集成测试。
## 目录结构
```
tests/
├── conftest.py # Pytest 配置和全局 fixtures
├── fixtures/ # 测试数据工厂
│ ├── __init__.py
│ ├── users.py # 用户工厂
│ ├── groups.py # 用户组工厂
│ └── objects.py # 对象(文件/目录)工厂
├── unit/ # 单元测试
│ ├── models/ # 模型测试
│ ├── utils/ # 工具测试
│ └── service/ # 服务测试
├── integration/ # 集成测试
│ ├── api/ # API 测试
│ └── middleware/ # 中间件测试
├── example_test.py # 示例测试(展示用法)
├── README.md # 原有文档
└── TESTING_GUIDE.md # 本文档
```
## 快速开始
### 1. 安装依赖
```bash
# 使用 uv 安装依赖
uv sync
### 2. 运行示例测试
```bash
# 运行示例测试,查看输出
pytest tests/example_test.py -v
```
### 3. 查看可用的 fixtures
```bash
# 列出所有可用的 fixtures
pytest --fixtures tests/conftest.py
```
## 可用的 Fixtures
### 数据库相关
| Fixture | 作用域 | 说明 |
|---------|--------|------|
| `test_engine` | function | SQLite 内存数据库引擎 |
| `db_session` | function | 异步数据库会话 |
| `initialized_db` | function | 已初始化的数据库(运行了 migration |
### HTTP 客户端
| Fixture | 作用域 | 说明 |
|---------|--------|------|
| `client` | function | 同步 TestClientFastAPI |
| `async_client` | function | 异步 httpx.AsyncClient |
### 测试用户
| Fixture | 作用域 | 返回值 | 说明 |
|---------|--------|--------|------|
| `test_user` | function | `dict[str, str \| UUID]` | 创建普通测试用户 |
| `admin_user` | function | `dict[str, str \| UUID]` | 创建管理员用户 |
返回的字典包含以下键:
- `id`: 用户 UUID
- `username`: 用户名
- `password`: 明文密码
- `token`: JWT 访问令牌
- `group_id`: 用户组 UUID
- `policy_id`: 存储策略 UUID
### 认证相关
| Fixture | 作用域 | 返回值 | 说明 |
|---------|--------|--------|------|
| `auth_headers` | function | `dict[str, str]` | 测试用户的认证请求头 |
| `admin_headers` | function | `dict[str, str]` | 管理员的认证请求头 |
### 测试数据
| Fixture | 作用域 | 返回值 | 说明 |
|---------|--------|--------|------|
| `test_directory` | function | `dict[str, UUID]` | 为测试用户创建目录结构 |
## 使用测试数据工厂
### UserFactory
```python
from tests.fixtures import UserFactory
# 创建普通用户
user = await UserFactory.create(
session,
group_id=group.id,
username="testuser",
password="password123",
nickname="测试用户",
score=100
)
# 创建管理员
admin = await UserFactory.create_admin(
session,
admin_group_id=admin_group.id,
username="admin"
)
# 创建被封禁用户
banned = await UserFactory.create_banned(
session,
group_id=group.id
)
# 创建有存储使用记录的用户
storage_user = await UserFactory.create_with_storage(
session,
group_id=group.id,
storage_bytes=1024 * 1024 * 100 # 100MB
)
```
### GroupFactory
```python
from tests.fixtures import GroupFactory
# 创建普通用户组(带选项)
group = await GroupFactory.create(
session,
name="测试组",
max_storage=1024 * 1024 * 1024 * 10, # 10GB
create_options=True, # 同时创建 GroupOptions
share_enabled=True,
web_dav_enabled=True
)
# 创建管理员组(自动创建完整的管理员选项)
admin_group = await GroupFactory.create_admin_group(
session,
name="管理员组"
)
# 创建有限制的用户组
limited_group = await GroupFactory.create_limited_group(
session,
max_storage=1024 * 1024 * 100, # 100MB
name="受限组"
)
# 创建免费用户组(最小权限)
free_group = await GroupFactory.create_free_group(session)
```
### ObjectFactory
```python
from tests.fixtures import ObjectFactory
# 创建用户根目录
root = await ObjectFactory.create_user_root(
session,
user,
policy.id
)
# 创建目录
folder = await ObjectFactory.create_folder(
session,
owner_id=user.id,
policy_id=policy.id,
parent_id=root.id,
name="documents"
)
# 创建文件
file = await ObjectFactory.create_file(
session,
owner_id=user.id,
policy_id=policy.id,
parent_id=folder.id,
name="test.txt",
size=1024
)
# 创建目录树(递归创建多层目录)
folders = await ObjectFactory.create_directory_tree(
session,
owner_id=user.id,
policy_id=policy.id,
root_id=root.id,
depth=3, # 3层深度
folders_per_level=2 # 每层2个目录
)
# 在目录中批量创建文件
files = await ObjectFactory.create_files_in_folder(
session,
owner_id=user.id,
policy_id=policy.id,
parent_id=folder.id,
count=10, # 创建10个文件
size_range=(1024, 1024 * 1024) # 1KB - 1MB
)
# 创建大文件(用于测试存储限制)
large_file = await ObjectFactory.create_large_file(
session,
owner_id=user.id,
policy_id=policy.id,
parent_id=folder.id,
size_mb=100
)
# 创建完整的嵌套结构(文档、媒体等)
structure = await ObjectFactory.create_nested_structure(
session,
owner_id=user.id,
policy_id=policy.id,
root_id=root.id
)
# 返回: {"documents": UUID, "work": UUID, "report": UUID, ...}
```
## 编写测试示例
### 单元测试
```python
import pytest
from sqlmodel.ext.asyncio.session import AsyncSession
from tests.fixtures import UserFactory, GroupFactory
@pytest.mark.unit
async def test_user_creation(db_session: AsyncSession):
"""测试用户创建功能"""
# 准备数据
group = await GroupFactory.create(db_session)
# 执行操作
user = await UserFactory.create(
db_session,
group_id=group.id,
username="testuser"
)
# 断言
assert user.id is not None
assert user.username == "testuser"
assert user.group_id == group.id
assert user.status is True
```
### 集成测试API
```python
import pytest
from httpx import AsyncClient
@pytest.mark.integration
async def test_user_login_api(
async_client: AsyncClient,
test_user: dict
):
"""测试用户登录 API"""
response = await async_client.post(
"/api/user/session",
json={
"username": test_user["username"],
"password": test_user["password"]
}
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert data["access_token"] == test_user["token"]
```
### 需要认证的测试
```python
import pytest
from httpx import AsyncClient
@pytest.mark.integration
async def test_protected_endpoint(
async_client: AsyncClient,
auth_headers: dict
):
"""测试需要认证的端点"""
response = await async_client.get(
"/api/user/me",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["username"] == "testuser"
```
### 使用 test_directory fixture
```python
import pytest
from httpx import AsyncClient
@pytest.mark.integration
async def test_list_directory(
async_client: AsyncClient,
auth_headers: dict,
test_directory: dict
):
"""测试获取目录列表"""
# test_directory 已创建了目录结构
response = await async_client.get(
f"/api/directory/{test_directory['documents']}",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "objects" in data
# 验证子目录存在
assert any(obj["name"] == "work" for obj in data["objects"])
assert any(obj["name"] == "personal" for obj in data["objects"])
```
## 运行测试
### 基本命令
```bash
# 运行所有测试
pytest
# 显示详细输出
pytest -v
# 运行特定测试文件
pytest tests/unit/models/test_user.py
# 运行特定测试函数
pytest tests/unit/models/test_user.py::test_user_creation
```
### 使用标记
```bash
# 只运行单元测试
pytest -m unit
# 只运行集成测试
pytest -m integration
# 运行慢速测试
pytest -m slow
# 运行除了慢速测试外的所有测试
pytest -m "not slow"
# 运行单元测试或集成测试
pytest -m "unit or integration"
```
### 测试覆盖率
```bash
# 生成覆盖率报告
pytest --cov=models --cov=routers --cov=middleware --cov=service --cov=utils
# 生成 HTML 覆盖率报告
pytest --cov=models --cov=routers --cov=utils --cov-report=html
# 查看 HTML 报告
# 在浏览器中打开 htmlcov/index.html
# 检查覆盖率是否达标80%
pytest --cov --cov-fail-under=80
```
### 并行运行
```bash
# 使用所有 CPU 核心
pytest -n auto
# 使用指定数量的核心
pytest -n 4
# 并行运行且显示详细输出
pytest -n auto -v
```
### 调试测试
```bash
# 显示更详细的输出
pytest -vv
# 显示 print 输出
pytest -s
# 进入调试模式(遇到失败时)
pytest --pdb
# 只运行上次失败的测试
pytest --lf
# 先运行上次失败的,再运行其他的
pytest --ff
```
## 测试标记
使用 pytest 标记来组织和筛选测试:
```python
# 单元测试
@pytest.mark.unit
async def test_something():
pass
# 集成测试
@pytest.mark.integration
async def test_api_endpoint():
pass
# 慢速测试(运行时间较长)
@pytest.mark.slow
async def test_large_dataset():
pass
# 组合标记
@pytest.mark.unit
@pytest.mark.slow
async def test_complex_calculation():
pass
# 跳过测试
@pytest.mark.skip(reason="暂时跳过")
async def test_work_in_progress():
pass
# 条件跳过
import sys
@pytest.mark.skipif(sys.platform == "win32", reason="仅限 Linux")
async def test_linux_only():
pass
```
## 测试最佳实践
### 1. 测试隔离
每个测试应该独立,不依赖其他测试的执行结果:
```python
# ✅ 好的实践
@pytest.mark.unit
async def test_user_creation(db_session: AsyncSession):
group = await GroupFactory.create(db_session)
user = await UserFactory.create(db_session, group_id=group.id)
assert user.id is not None
# ❌ 不好的实践(依赖全局状态)
global_user = None
@pytest.mark.unit
async def test_create_user(db_session: AsyncSession):
global global_user
group = await GroupFactory.create(db_session)
global_user = await UserFactory.create(db_session, group_id=group.id)
@pytest.mark.unit
async def test_update_user(db_session: AsyncSession):
# 依赖前一个测试的结果
assert global_user is not None
global_user.nickname = "Updated"
await global_user.save(db_session)
```
### 2. 使用工厂而非手动创建
```python
# ✅ 好的实践
user = await UserFactory.create(db_session, group_id=group.id)
# ❌ 不好的实践
user = User(
username="test",
password=Password.hash("password"),
group_id=group.id,
status=True,
storage=0,
score=100,
# ... 更多字段
)
user = await user.save(db_session)
```
### 3. 清晰的断言
```python
# ✅ 好的实践
assert user.username == "testuser", "用户名应该是 testuser"
assert user.status is True, "新用户应该是激活状态"
# ❌ 不好的实践
assert user # 不清楚在验证什么
```
### 4. 测试异常情况
```python
import pytest
@pytest.mark.unit
async def test_duplicate_username(db_session: AsyncSession):
"""测试创建重复用户名"""
group = await GroupFactory.create(db_session)
# 创建第一个用户
await UserFactory.create(
db_session,
group_id=group.id,
username="duplicate"
)
# 尝试创建同名用户应该失败
with pytest.raises(Exception): # 或更具体的异常类型
await UserFactory.create(
db_session,
group_id=group.id,
username="duplicate"
)
```
### 5. 适当的测试粒度
```python
# ✅ 好的实践:一个测试验证一个行为
@pytest.mark.unit
async def test_user_creation(db_session: AsyncSession):
"""测试用户创建"""
# 只测试创建
@pytest.mark.unit
async def test_user_authentication(db_session: AsyncSession):
"""测试用户认证"""
# 只测试认证
# ❌ 不好的实践:一个测试做太多事
@pytest.mark.unit
async def test_user_everything(db_session: AsyncSession):
"""测试用户的所有功能"""
# 创建、更新、删除、认证...全都在一个测试里
```
## 常见问题
### Q: 测试失败时如何调试?
```bash
# 使用 -vv 显示更详细的输出
pytest -vv
# 使用 -s 显示 print 语句
pytest -s
# 使用 --pdb 在失败时进入调试器
pytest --pdb
# 组合使用
pytest -vvs --pdb
```
### Q: 如何只运行某些测试?
```bash
# 按标记运行
pytest -m unit
# 按文件运行
pytest tests/unit/models/
# 按测试名称模糊匹配
pytest -k "user" # 运行所有名称包含 "user" 的测试
# 组合条件
pytest -m unit -k "not slow"
```
### Q: 数据库会话相关错误?
确保使用正确的 fixture
```python
# ✅ 正确
async def test_something(db_session: AsyncSession):
user = await User.get(db_session, User.id == some_id)
# ❌ 错误:没有传入 session
async def test_something():
user = await User.get(User.id == some_id) # 会失败
```
### Q: 异步测试不工作?
确保使用 pytest-asyncio 标记或配置了 asyncio_mode
```python
# pyproject.toml 中已配置 asyncio_mode = "auto"
# 所以不需要 @pytest.mark.asyncio
async def test_async_function(db_session: AsyncSession):
# 会自动识别为异步测试
pass
```
### Q: 如何测试需要认证的端点?
使用 `auth_headers` fixture
```python
async def test_protected_route(
async_client: AsyncClient,
auth_headers: dict
):
response = await async_client.get(
"/api/protected",
headers=auth_headers
)
assert response.status_code == 200
```
## 参考资料
- [Pytest 官方文档](https://docs.pytest.org/)
- [pytest-asyncio 文档](https://pytest-asyncio.readthedocs.io/)
- [FastAPI 测试指南](https://fastapi.tiangolo.com/tutorial/testing/)
- [httpx 测试客户端](https://www.python-httpx.org/advanced/#calling-into-python-web-apps)
- [SQLModel 文档](https://sqlmodel.tiangolo.com/)
## 贡献
如果您发现文档中的错误或有改进建议,请:
1. 在项目中创建 Issue
2. 提交 Pull Request
3. 更新相关文档
---
更新时间: 2025-12-19