207 lines
7.4 KiB
Markdown
207 lines
7.4 KiB
Markdown
# Testing Patterns
|
||
|
||
**Analysis Date:** 2026-04-07
|
||
|
||
## Test Framework
|
||
|
||
**Runner:**
|
||
- Backend: `pytest` (>=8.0.0, 定义于 `backend/pyproject.toml`)
|
||
- Frontend E2E: `@playwright/test` (定义于 `frontend/package.json`)
|
||
- Frontend 轻量单测: Node 内置 `node:test` + `node:assert/strict`(示例见 `frontend/src/core/api/stream-mode.test.ts`)
|
||
- Config: `frontend/playwright.config.ts`(E2E);Backend 未检测到独立 `pytest.ini`/`tox.ini`
|
||
|
||
**Assertion Library:**
|
||
- Backend: `pytest` 原生断言
|
||
- Frontend E2E: Playwright `expect`
|
||
- Frontend 轻量单测: `node:assert/strict`
|
||
|
||
**Run Commands:**
|
||
```bash
|
||
cd backend && make test # 运行后端测试(pytest tests/ -v)
|
||
cd backend && make lint # 后端静态检查(ruff check + format --check)
|
||
cd frontend && pnpm test:e2e # 运行 Playwright E2E
|
||
cd frontend && pnpm test:e2e:ui # Playwright UI 模式
|
||
cd frontend && pnpm lint && pnpm typecheck # 前端质量门禁
|
||
```
|
||
|
||
## Test File Organization
|
||
|
||
**Location:**
|
||
- Backend 测试集中在 `backend/tests/`,按模块与能力拆分(如 `test_client.py`、`test_stream_bridge.py`)。
|
||
- Frontend E2E 在 `frontend/tests/e2e/`,辅助函数在 `frontend/tests/e2e/support/`。
|
||
- Frontend 轻量模块测试与实现同目录共置(如 `frontend/src/core/uploads/*.test.mjs`)。
|
||
|
||
**Naming:**
|
||
- Python: `test_*.py`
|
||
- Playwright: `*.spec.ts`
|
||
- Node 内置测试: `*.test.ts` / `*.test.mjs`
|
||
|
||
**Structure:**
|
||
```text
|
||
backend/tests/test_*.py
|
||
frontend/tests/e2e/*.spec.ts
|
||
frontend/tests/e2e/support/*.ts
|
||
frontend/src/**/**.test.ts
|
||
frontend/src/**/**.test.mjs
|
||
```
|
||
|
||
## Test Structure
|
||
|
||
**Suite Organization:**
|
||
```python
|
||
# backend/tests/test_stream_bridge.py
|
||
@pytest.fixture
|
||
def bridge() -> MemoryStreamBridge:
|
||
return MemoryStreamBridge(queue_maxsize=256)
|
||
|
||
@pytest.mark.anyio
|
||
async def test_publish_subscribe(bridge: MemoryStreamBridge):
|
||
...
|
||
```
|
||
|
||
```typescript
|
||
// frontend/tests/e2e/input-and-compose.spec.ts
|
||
test.describe("聊天工作台 / 输入区与发送", () => {
|
||
test("DF-INPUT-001 ...", async ({ page }, testInfo) => {
|
||
...
|
||
});
|
||
});
|
||
```
|
||
|
||
**Patterns:**
|
||
- Setup pattern:
|
||
- Backend 使用 `conftest.py` 全局 fixture 与 import/mock 预处理(`backend/tests/conftest.py`)。
|
||
- E2E 使用 `chat-helpers.ts` 封装 URL 构建、页面打开、发送消息、线程前置校验。
|
||
- Teardown pattern:
|
||
- Backend 通过 `tmp_path` + `monkeypatch` 隔离文件系统与全局单例(`backend/tests/test_client_e2e.py`)。
|
||
- Frontend Node 测试手动还原 `globalThis.fetch`/`console.warn`。
|
||
- Assertion pattern:
|
||
- Backend 侧重行为与事件序列断言(含异常、边界、并发)。
|
||
- E2E 使用 `expect.poll` 与语义选择器(`getByTestId`、`getByRole`)减少时序抖动。
|
||
|
||
## Mocking
|
||
|
||
**Framework:** `unittest.mock` + `pytest.monkeypatch`(Backend);函数级覆写全局对象(Frontend Node tests)
|
||
|
||
**Patterns:**
|
||
```python
|
||
# backend/tests/test_model_factory.py
|
||
def _patch_factory(monkeypatch, app_config, model_class=FakeChatModel):
|
||
monkeypatch.setattr(factory_module, "get_app_config", lambda: app_config)
|
||
monkeypatch.setattr(factory_module, "resolve_class", lambda path, base: model_class)
|
||
```
|
||
|
||
```typescript
|
||
// frontend/src/core/uploads/prompt-input-files.test.mjs
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = async () => { throw new Error("network down"); };
|
||
...
|
||
globalThis.fetch = originalFetch;
|
||
```
|
||
|
||
**What to Mock:**
|
||
- 外部依赖与昂贵路径:LLM、网络请求、全局单例、文件系统路径。
|
||
- 与测试目标无关的中间件/副作用(如标题生成、内存队列)应在测试中关闭。
|
||
|
||
**What NOT to Mock:**
|
||
- 核心业务状态流与事件序列(例如 `MemoryStreamBridge` 的 publish/subscribe/end 行为)。
|
||
- E2E 页面路由与关键 UI 交互链路(应尽量走真实页面流程)。
|
||
|
||
## Fixtures and Factories
|
||
|
||
**Test Data:**
|
||
```python
|
||
# backend/tests/test_client.py
|
||
@pytest.fixture
|
||
def mock_app_config():
|
||
model = MagicMock()
|
||
model.name = "test-model"
|
||
...
|
||
return config
|
||
```
|
||
|
||
```typescript
|
||
// frontend/tests/e2e/support/chat-helpers.ts
|
||
export function newChatEntry(threadId: string) { ... }
|
||
export async function openChat(page: Page, url: string, options?: { expectInput?: boolean }) { ... }
|
||
```
|
||
|
||
**Location:**
|
||
- Backend 全局 fixture: `backend/tests/conftest.py`
|
||
- Backend 模块级 fixture/factory: 各 `backend/tests/test_*.py`
|
||
- Frontend E2E fixture/helper: `frontend/tests/e2e/support/chat-helpers.ts`
|
||
|
||
## Coverage
|
||
|
||
**Requirements:** 未检测到覆盖率阈值与强制 coverage 工具(无 `coverage.py`/`c8`/`nyc` 配置;CI 未执行 coverage 报告)。
|
||
|
||
**View Coverage:**
|
||
```bash
|
||
Not applicable(仓库未提供标准覆盖率命令)
|
||
```
|
||
|
||
## Test Types
|
||
|
||
**Unit Tests:**
|
||
- Backend: 大量纯单测,重度使用 mock 与 monkeypatch,覆盖配置解析、middleware、tools、runtime 细节。
|
||
- Frontend: 少量模块单测,采用 Node 内置测试框架,主要覆盖上传与 stream mode 边界。
|
||
|
||
**Integration Tests:**
|
||
- Backend: `test_client_e2e.py` 定位为“中层集成”,走真实模块链路并通过环境控制是否触发真实 LLM。
|
||
- Backend: 存在 `*_live.py` 用于更高成本的真实依赖验证(如 `backend/tests/test_client_live.py`、`backend/tests/test_create_deerflow_agent_live.py`)。
|
||
|
||
**E2E Tests:**
|
||
- Frontend: Playwright 场景化用例,覆盖路由、输入、历史、artifact 面板等用户路径(`frontend/tests/e2e/*.spec.ts`)。
|
||
- 执行依赖线程相关环境变量(`FRONTEND_E2E_THREAD_ID` 等),缺失时通过 `testInfo.skip` 跳过。
|
||
|
||
## Common Patterns
|
||
|
||
**Async Testing:**
|
||
```python
|
||
@pytest.mark.anyio
|
||
async def test_heartbeat(bridge: MemoryStreamBridge):
|
||
await asyncio.wait_for(consumer(), timeout=2.0)
|
||
```
|
||
|
||
```typescript
|
||
await expect
|
||
.poll(async () => await page.locator(".is-user, .is-assistant").count(), { timeout: 30_000 })
|
||
.toBeGreaterThan(0);
|
||
```
|
||
|
||
**Error Testing:**
|
||
```python
|
||
with pytest.raises(ValueError, match="Invalid agent name"):
|
||
DeerFlowClient(agent_name="invalid name with spaces!")
|
||
```
|
||
|
||
```typescript
|
||
globalThis.fetch = async () => new Response("missing", { status: 404, statusText: "Not Found" });
|
||
const converted = await promptInputFilePartToFile(...);
|
||
assert.equal(converted, null);
|
||
```
|
||
|
||
## 分层执行与覆盖风险(可执行建议)
|
||
|
||
- 当前可稳定落地的分层执行顺序:
|
||
1. `cd backend && make lint && make test`
|
||
2. `cd frontend && pnpm format && pnpm lint && pnpm typecheck && pnpm build`
|
||
3. `cd frontend && pnpm test:e2e`(在具备线程测试数据的环境)
|
||
- 关键风险:
|
||
- Frontend 单元测试未接入统一脚本与 CI(`package.json` 无 `test` 脚本,CI 未跑 `test:e2e`)。
|
||
- 覆盖率无门槛,变更后“测试数量增长”不等于“关键路径被覆盖”。
|
||
- E2E 对外部线程数据依赖较强,易出现“跳过即通过”。
|
||
- 建议后续规划优先级:
|
||
1. 为 Frontend 增加统一单测命令并纳入 CI。
|
||
2. 在 CI 增加最小 E2E smoke(可用固定 seed 数据或 mock 后端)。
|
||
3. 引入覆盖率报告(先观测,再设阈值)。
|
||
|
||
## 简短结论
|
||
|
||
- 仓库已具备较完整 Backend 测试金字塔与前端场景化 E2E 基础,但“前端自动化测试接入 CI 不完整、覆盖率无硬约束”是当前质量收敛的主要短板。
|
||
- 后续规划应优先补齐前端测试执行入口和 CI 集成,再推进覆盖率治理,才能让测试从“存在”转为“持续有效”。
|
||
|
||
---
|
||
|
||
*Testing analysis: 2026-04-07*
|