# 测试指南 本文档定义新 SaaS 项目的测试策略、测试规范和最佳实践。 --- ## 1. 测试策略 ### 1.1 测试金字塔 ``` E2E Tests (少量) / \ Integration Tests (中量) / \ Unit Tests (大量,按需) ``` **当前阶段(Phase 1)**: - **集成测试优先** - 覆盖完整业务流程 - 单元测试辅助 - 覆盖复杂逻辑 - E2E 测试占位 - Phase 2-3 补充 **原因**: - 集成测试验证架构正确性 - 集成测试覆盖核心业务流程 - In-Memory 实现使得集成测试成本低 --- ## 2. 测试工具 **测试框架**: pytest **测试环境**: in-memory 或 SQLite **覆盖率**: pytest-cov ```bash # 运行所有测试 pytest tests/integration/ -v # 运行指定测试 pytest tests/integration/test_projects.py -v # 运行带覆盖率 pytest --cov=packages --cov=apps --cov-report=html # 运行快速测试(跳过慢速测试) pytest -m "not slow" ``` --- ## 3. 测试结构 ``` tests/ ├── conftest.py # 共享 fixtures ├── integration/ # 集成测试 │ ├── test_projects.py │ ├── test_ingest_pipeline.py │ ├── test_classification_pipeline.py │ └── test_upload_pipeline.py ├── unit/ # 单元测试 │ ├── test_project_entity.py │ └── test_asset_validation.py └── e2e/ # 端到端测试(占位) └── README.md ``` --- ## 4. 测试命名 ### 4.1 测试文件 ``` test_{module_name}.py ``` ### 4.2 测试函数 **推荐格式**:`test_{what}_{condition}` ```python # ✅ 正确 def test_create_project_with_valid_name(): pass def test_create_project_with_empty_name_should_fail(): pass def test_list_projects_by_workspace(): pass def test_ingest_asset_updates_job_status_to_completed(): pass # ❌ 错误 def test1(): pass def test_project(): pass def test_stuff(): pass ``` --- ## 5. 测试结构(AAA 模式) **Arrange - Act - Assert** ```python def test_create_project(): # Arrange(准备) workspace_id = "ws-1" name = "测试项目" repository = InMemoryProjectRepository() use_case = CreateProjectUseCase(repository) # Act(执行) project = use_case.execute( CreateProjectCommand(workspace_id=workspace_id, name=name) ) # Assert(断言) assert project.name == name assert project.workspace_id == workspace_id assert project.id != "" ``` --- ## 6. 集成测试 ### 6.1 测试完整业务流程 ```python def test_upload_to_asset_full_pipeline(): """测试完整上传链路:上传 → storage → 入库任务 → worker → asset 创建。""" # Arrange job_repo = InMemoryIngestJobRepository() asset_repo = InMemoryAssetRepository() # Act - 提交入库任务 use_case = SubmitIngestJobUseCase(job_repo) job = use_case.execute( SubmitIngestJobCommand( workspace_id="ws-1", project_id="proj-1", library_id="lib-1", storage_key="uploads/abc123/video.mp4", ) ) # Act - 模拟 worker 处理 result = simulate_ingest_asset(job.id, job_repo, asset_repo) # Assert - 验证任务完成 assert result["status"] == "completed" updated_job = job_repo.get(job.id) assert updated_job.status == IngestJobStatus.COMPLETED # Assert - 验证 asset 创建 assets = asset_repo.list_by_library("lib-1") assert len(assets) == 1 assert assets[0].storage_key == "uploads/abc123/video.mp4" ``` ### 6.2 使用 In-Memory 实现 ```python from packages.adapters.in_memory import ( InMemoryProjectRepository, InMemoryAssetRepository, InMemoryIngestJobRepository, ) def test_something(): # 使用 in-memory 实现,快速且无外部依赖 repository = InMemoryProjectRepository() # ... ``` --- ## 7. 测试场景覆盖 ### 7.1 正常场景(Happy Path) ```python def test_create_project_with_valid_data(): """测试创建项目(正常场景)。""" pass def test_list_projects_returns_all_projects(): """测试查询项目列表(正常场景)。""" pass ``` ### 7.2 边界条件(Boundary Cases) ```python def test_create_project_with_empty_name_should_fail(): """测试创建项目(项目名为空应失败)。""" with pytest.raises(ValueError, match="项目名称不能为空"): Project.create(workspace_id="ws-1", name="") def test_create_project_with_max_length_name(): """测试创建项目(项目名最大长度)。""" long_name = "a" * 100 project = Project.create(workspace_id="ws-1", name=long_name) assert len(project.name) == 100 def test_create_project_with_too_long_name_should_fail(): """测试创建项目(项目名超长应失败)。""" too_long_name = "a" * 101 with pytest.raises(ValueError, match="项目名称不能超过"): Project.create(workspace_id="ws-1", name=too_long_name) ``` ### 7.3 异常场景(Error Cases) ```python def test_get_nonexistent_project_returns_none(): """测试查询不存在的项目(应返回 None)。""" repository = InMemoryProjectRepository() project = repository.get("nonexistent-id") assert project is None def test_update_nonexistent_project_should_fail(): """测试更新不存在的项目(应失败)。""" repository = InMemoryProjectRepository() with pytest.raises(ValueError, match="项目.*不存在"): repository.update(Project(id="nonexistent-id", ...)) ``` ### 7.4 并发场景(Concurrency) ```python def test_concurrent_create_same_project(): """测试并发创建相同项目(应处理冲突)。""" # Phase 2 补充 pass ``` ### 7.5 回归场景(Regression) ```python def test_regression_ingest_job_status_not_reset(): """回归测试:验证 IngestJob 状态不会被意外重置(bug #123)。""" pass ``` --- ## 8. Fixtures ### 8.1 共享 Fixtures 在 `conftest.py` 中定义: ```python import pytest from packages.adapters.in_memory import InMemoryProjectRepository @pytest.fixture def project_repository(): """项目仓储 fixture。""" return InMemoryProjectRepository() @pytest.fixture def sample_project(): """示例项目 fixture。""" return Project.create( workspace_id="ws-1", name="测试项目", description="这是一个测试项目" ) ``` 使用: ```python def test_create_project(project_repository): """测试创建项目。""" project = Project.create(workspace_id="ws-1", name="新项目") saved = project_repository.create(project) assert saved.id == project.id def test_list_projects(project_repository, sample_project): """测试查询项目列表。""" project_repository.create(sample_project) projects = project_repository.list_by_workspace("ws-1") assert len(projects) == 1 ``` --- ## 9. 测试标记(Markers) ```python import pytest @pytest.mark.slow def test_large_dataset(): """慢速测试(大数据集)。""" pass @pytest.mark.integration def test_full_pipeline(): """集成测试。""" pass @pytest.mark.unit def test_entity_validation(): """单元测试。""" pass ``` 运行特定标记的测试: ```bash # 只运行集成测试 pytest -m integration # 跳过慢速测试 pytest -m "not slow" ``` --- ## 10. 断言(Assertions) ### 10.1 基本断言 ```python # 相等 assert result == expected # 不相等 assert result != unexpected # 包含 assert item in collection assert key in dictionary # 真值 assert condition assert not condition ``` ### 10.2 异常断言 ```python import pytest # 验证抛出异常 with pytest.raises(ValueError): Project.create(workspace_id="ws-1", name="") # 验证异常消息 with pytest.raises(ValueError, match="项目名称不能为空"): Project.create(workspace_id="ws-1", name="") ``` ### 10.3 近似断言 ```python import pytest # 浮点数近似相等 assert result == pytest.approx(0.85, rel=1e-2) ``` --- ## 11. 测试数据 ### 11.1 避免硬编码 ID ```python # ❌ 错误 def test_create_project(): project = Project(id="proj-123", ...) # ✅ 正确 def test_create_project(): project = Project.create(workspace_id="ws-1", name="测试项目") assert project.id != "" # ID 由系统生成 ``` ### 11.2 使用有意义的测试数据 ```python # ❌ 错误 def test_create_project(): project = Project.create(workspace_id="a", name="b") # ✅ 正确 def test_create_project(): project = Project.create( workspace_id="ws-test-001", name="电商平台项目", description="2026 年 Q2 电商平台重构项目" ) ``` --- ## 12. 测试覆盖率 ### 12.1 目标覆盖率 - **Domain 层**: 100% - **Application 层**: 90%+ - **Adapters 层**: 80%+ - **API 层**: 70%+ ### 12.2 查看覆盖率 ```bash # 生成 HTML 报告 pytest --cov=packages --cov=apps --cov-report=html # 打开报告 open htmlcov/index.html # macOS/Linux start htmlcov/index.html # Windows ``` --- ## 13. 测试最佳实践 ### 13.1 每个测试独立 ```python # ✅ 正确 - 每个测试独立 def test_create_project(): repository = InMemoryProjectRepository() project = Project.create(workspace_id="ws-1", name="项目 1") repository.create(project) def test_list_projects(): repository = InMemoryProjectRepository() # 新实例 project = Project.create(workspace_id="ws-1", name="项目 2") repository.create(project) projects = repository.list_by_workspace("ws-1") assert len(projects) == 1 ``` ### 13.2 测试一件事 ```python # ❌ 错误 - 测试多件事 def test_project_crud(): repository = InMemoryProjectRepository() # 创建 project = Project.create(...) repository.create(project) # 查询 found = repository.get(project.id) # 更新 found.name = "新名称" repository.update(found) # 删除 repository.delete(project.id) # ✅ 正确 - 拆分成多个测试 def test_create_project(): pass def test_get_project(): pass def test_update_project(): pass def test_delete_project(): pass ``` ### 13.3 避免测试实现细节 ```python # ❌ 错误 - 测试实现细节 def test_project_repository_uses_dict(): repository = InMemoryProjectRepository() assert isinstance(repository._items, dict) # ✅ 正确 - 测试行为 def test_project_repository_stores_project(): repository = InMemoryProjectRepository() project = Project.create(workspace_id="ws-1", name="项目") repository.create(project) found = repository.get(project.id) assert found is not None assert found.name == "项目" ``` --- ## 14. 持续集成(Phase 2) ```yaml # .github/workflows/test.yml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: '3.12' - run: pip install -r requirements.txt - run: pytest tests/ --cov=packages --cov=apps ``` --- **最后更新**: 2026-06-15 **版本**: v1.0