Skip to content

Commit f4a89e7

Browse files
gxklclaude
andcommitted
feat(skills): add egg-unittest skill with references and evals
Add unittest skill covering HTTP test, Service/DI test, Mock patterns, BackgroundTaskHelper test, and EventBus test. Update entry skill routing for unittest queries. Add cross-references from egg-controller and egg-core skills. Add 24 eval cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bebcef0 commit f4a89e7

13 files changed

Lines changed: 847 additions & 37 deletions

File tree

packages/skills/egg-controller/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,5 @@ allowed-tools: Read
101101
- `references/middleware.md` - Middleware 中间件(函数式 + AOP)
102102

103103
核心概念(`egg-core` skill):模块、依赖注入、对象生命周期
104+
105+
单元测试(`egg-unittest` skill):HTTP 接口测试、Service 测试、Mock

packages/skills/egg-core/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,5 @@ AOP 用于将日志、鉴权、缓存、事务等横切关注点从业务代码
244244
- 请求后异步任务(BackgroundTaskHelper),请参阅:`references/background-task.md`
245245
- 事件总线(EventBus),请参阅:`references/eventbus.md`
246246
- AOP 切面编程(Advice、Pointcut、Crosscut),请参阅:`references/aop.md`
247+
248+
单元测试(`egg-unittest` skill):Service 测试、BackgroundTask 测试、EventBus 测试

packages/skills/egg-core/references/background-task.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,9 @@ BackgroundTaskHelper 的作用是:
113113
4. 等待结束后才释放上下文
114114

115115
这就是为什么必须用 `backgroundTaskHelper.run()` 而不是 `setTimeout` — 后者绕过了框架的上下文生命周期管理,执行时上下文可能已被释放。
116+
117+
---
118+
119+
## 单元测试
120+
121+
BackgroundTaskHelper 的测试方法参考 `egg-unittest` skill 的 `references/background-task-test.md`

packages/skills/egg-core/references/eventbus.md

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -135,39 +135,4 @@ export class BatchService {
135135

136136
## 单元测试
137137

138-
使用 `app.getEventWaiter()` 等待事件被处理完成,避免使用 `sleep`
139-
140-
```typescript
141-
import { describe, it, expect } from 'vitest';
142-
import { mm, type MockApplication } from '@eggjs/mock';
143-
import { OrderService } from './path/to/OrderService.ts';
144-
145-
describe('order events', () => {
146-
let app: MockApplication;
147-
148-
it('should emit orderCreated event', async () => {
149-
await app.mockModuleContextScope(async (ctx) => {
150-
const orderService = await ctx.getEggObject(OrderService);
151-
const eventWaiter = await app.getEventWaiter();
152-
153-
// 准备等待事件
154-
const eventPromise = eventWaiter.await('orderCreated');
155-
156-
// 触发业务逻辑
157-
await orderService.createOrder('user1', []);
158-
159-
// 等待事件处理完成
160-
await eventPromise;
161-
162-
// 断言 handler 的执行结果
163-
});
164-
});
165-
});
166-
```
167-
168-
**EventWaiter API:**
169-
170-
- `eventWaiter.await('eventName')` — 等待指定事件触发
171-
- `eventWaiter.awaitFirst('event1', 'event2')` — 等待多个事件中最先触发的一个
172-
173-
**注意:** `eventWaiter.await()` 必须在 `emit()` 之前调用(先注册等待,再触发事件),否则可能错过事件。
138+
EventBus 的测试方法参考 `egg-unittest` skill 的 `references/eventbus-test.md`
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
---
2+
name: egg-unittest
3+
description: 本技能用于编写 EGG 应用的单元测试。覆盖 HTTP 接口测试、Service/DI 对象测试、Mock 数据模拟、BackgroundTask 和 EventBus 测试。使用 @eggjs/mock、app.httpRequest()、app.getEggObject()、mm() 等 API。
4+
allowed-tools: Read
5+
---
6+
7+
# EGG 单元测试
8+
9+
---
10+
11+
## 原理
12+
13+
`egg-bin test` 使用 Vitest 运行测试,自动完成以下工作:
14+
15+
- 以当前项目目录为 baseDir,创建并启动一个 MockApplication 实例(即 `app`
16+
- 注入 `@eggjs/mock/setup_vitest` 管理 app 生命周期(`beforeAll` 启动 app、`afterEach` 恢复 mock、`afterAll` 关闭 app)
17+
- 注入 Vitest 全局变量(`describe``it``beforeAll` 等),无需手动 import
18+
测试代码中通过 `import { app, mm } from '@eggjs/mock/bootstrap'` 获取已启动的 app 实例和 mock 工具,直接使用即可。
19+
20+
---
21+
22+
## 配置检查
23+
24+
确保项目中有以下配置:
25+
26+
**package.json:**
27+
28+
```json
29+
{
30+
"scripts": {
31+
"test": "egg-bin test"
32+
},
33+
"devDependencies": {
34+
"@eggjs/bin": "^8",
35+
"@eggjs/mock": "^8"
36+
}
37+
}
38+
```
39+
40+
**测试文件约定:**
41+
42+
- 测试目录:`test/`
43+
- 文件命名:`*.test.ts`
44+
45+
**自定义 setup 文件(可选):**
46+
47+
如果存在 `test/.setup.ts`,egg-bin 会自动将其加入 vitest setupFiles,在 `@eggjs/mock/setup_vitest` 之前执行(即 app 启动之前)。可用于设置环境变量等全局初始化:
48+
49+
```typescript
50+
// test/.setup.ts
51+
beforeAll(() => {
52+
process.env.SOME_CONFIG = 'test-value';
53+
});
54+
```
55+
56+
---
57+
58+
## 简单示例
59+
60+
### HTTP 接口测试
61+
62+
```typescript
63+
import assert from 'node:assert';
64+
import { app } from '@eggjs/mock/bootstrap';
65+
66+
describe('test/controller/home.test.ts', () => {
67+
it('should GET /', () => {
68+
return app.httpRequest()
69+
.get('/')
70+
.expect(200)
71+
.expect('hello world');
72+
});
73+
});
74+
```
75+
76+
### Service 测试
77+
78+
```typescript
79+
import assert from 'node:assert';
80+
import { app } from '@eggjs/mock/bootstrap';
81+
import { UserService } from '../app/modules/user/UserService.ts';
82+
83+
describe('test/service/user.test.ts', () => {
84+
it('should get user', async () => {
85+
const userService = await app.getEggObject(UserService);
86+
const user = await userService.getById('1');
87+
assert(user);
88+
assert.equal(user.name, 'test');
89+
});
90+
});
91+
```
92+
93+
---
94+
95+
## 测试场景决策树
96+
97+
```
98+
要测什么?
99+
100+
1. HTTP 接口(GET/POST/PUT/DELETE)?
101+
→ 参考 references/http-test.md
102+
103+
2. Service / DI 对象的方法?
104+
→ 参考 references/service-test.md
105+
106+
3. 需要 mock 外部依赖?(HTTP 调用、Service 方法、Session、CSRF)
107+
→ 参考 references/mock.md
108+
109+
4. BackgroundTaskHelper(后台异步任务)?
110+
→ 参考 references/background-task-test.md
111+
112+
5. EventBus(事件驱动)?
113+
→ 参考 references/eventbus-test.md
114+
```
115+
116+
---
117+
118+
## 快速参考
119+
120+
| API | 说明 |
121+
| ---------------------------------------------------- | ------------------------------- |
122+
| `import { app, mm } from '@eggjs/mock/bootstrap'` | 标准测试入口 |
123+
| `app.httpRequest().get('/path').expect(200)` | HTTP 接口测试 |
124+
| `app.getEggObject(Class)` | 获取 Singleton 实例 |
125+
| `app.mockModuleContextScope(async (ctx) => { ... })` | ContextProto 测试作用域 |
126+
| `mm(Class.prototype, 'method', fn)` | Mock Proto 方法 |
127+
| `app.mockCsrf()` | 跳过 CSRF 校验(POST 测试必备) |
128+
| `app.mockHttpclient(url, data)` | Mock 外部 HTTP 调用 |
129+
| `app.getEventWaiter()` | 获取 EventBus 事件等待器 |
130+
131+
---
132+
133+
## 常见错误
134+
135+
| 错误写法 | 正确写法 | 说明 |
136+
| -------------------------------------- | ----------------------------------------------------------- | ------------------------------------- |
137+
| `import { app } from 'egg'` | `import { app } from '@eggjs/mock/bootstrap'` | 测试使用 mock 包 |
138+
| `before()` / `after()` | `beforeAll()` / `afterAll()` | Vitest 钩子,不是 Mocha |
139+
| POST 测试报 403 |`app.mockCsrf()` | 安全插件默认开启 CSRF |
140+
| 手动写 `afterEach(mm.restore)` | 不需要 | egg-bin 自动注入 mock 恢复 |
141+
| `app.getEggObject()` 获取 ContextProto |`app.mockModuleContextScope()` 内用 `ctx.getEggObject()` | `app.getEggObject` 只能获取 Singleton |
142+
| 代码写在 describe 内、hooks 外 | 放入 `beforeAll` / `beforeEach` | describe 体在加载阶段就执行 |
143+
| `await app.ready()` 配合 bootstrap | 不需要 | bootstrap 自动处理生命周期 |
144+
145+
---
146+
147+
## 参考资料
148+
149+
- `references/http-test.md` — HTTP 接口测试
150+
- `references/service-test.md` — Service/DI 对象测试
151+
- `references/mock.md` — Mock 模式
152+
- `references/background-task-test.md` — BackgroundTaskHelper 测试
153+
- `references/eventbus-test.md` — EventBus 测试
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# BackgroundTaskHelper 测试
2+
3+
## 常见错误
4+
5+
| 错误写法 | 正确写法 | 说明 |
6+
| -------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------- |
7+
| 不等待就断言 |`mockModuleContextScope`(自动等待)或 `app.backgroundTasksFinished()`(手动等待) | 后台任务异步执行,必须等待完成后再断言 |
8+
|`mockModuleContextScope` 回调内断言后台任务结果 |`mockModuleContextScope` 返回后断言 | 回调内任务尚未完成,返回后才会等待完成 |
9+
|`TimerUtil.sleep` 等待 |`app.backgroundTasksFinished()` | sleep 时间不确定,`backgroundTasksFinished` 精确等待 |
10+
11+
---
12+
13+
## 使用 mockModuleContextScope
14+
15+
`mockModuleContextScope` 退出时会自动等待所有后台任务完成(内部触发 `doPreDestroy`),scope 退出后直接断言即可:
16+
17+
```typescript
18+
import assert from 'node:assert';
19+
import { app } from '@eggjs/mock/bootstrap';
20+
import { CountService } from '../app/modules/count/CountService.ts';
21+
22+
it('should complete background task', async () => {
23+
await app.mockModuleContextScope(async (ctx) => {
24+
const countService = await ctx.getEggObject(CountService);
25+
// countService 内部通过 backgroundTaskHelper.run() 触发后台任务
26+
await countService.doSomething();
27+
});
28+
29+
// scope 退出后,后台任务已完成,直接断言
30+
const countService = await app.getEggObject(CountService);
31+
assert.equal(countService.count, 1);
32+
});
33+
```
34+
35+
## 使用 backgroundTasksFinished
36+
37+
不通过 `mockModuleContextScope` 触发的场景(如 HTTP 接口测试),scope 退出的自动等待机制不适用,需要手动调用 `app.backgroundTasksFinished()` 等待所有后台任务完成后,再做断言:
38+
39+
```typescript
40+
import assert from 'node:assert';
41+
import { app } from '@eggjs/mock/bootstrap';
42+
import { CountService } from '../app/modules/count/CountService.ts';
43+
44+
it('should complete background task', async () => {
45+
await app.httpRequest()
46+
.get('/api/trigger-task')
47+
.expect(200);
48+
49+
// 等待后台任务完成
50+
await app.backgroundTasksFinished();
51+
52+
const countService = await app.getEggObject(CountService);
53+
assert.equal(countService.count, 1);
54+
});
55+
```
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# EventBus 测试
2+
3+
## 常见错误
4+
5+
| 错误写法 | 正确写法 | 说明 |
6+
| -------------------------------- | -------------------------------------------------- | -------------------------------------------- |
7+
| 先 emit 再 `eventWaiter.await()` |`eventWaiter.await()` 再触发业务逻辑 | await 注册监听器,必须在事件发出前 |
8+
| 不等待事件处理完成就断言 | 使用 `eventWaiter.await('eventName')` 等待后再断言 | handler 异步执行,不等待则断言时可能尚未完成 |
9+
10+
---
11+
12+
## 基本测试模式
13+
14+
使用 `app.getEventWaiter()` 等待事件被处理完成:
15+
16+
```typescript
17+
import assert from 'node:assert';
18+
import { app, mm } from '@eggjs/mock/bootstrap';
19+
import { HelloService } from '../app/modules/hello/HelloService.ts';
20+
import { HelloHandler } from '../app/modules/hello/HelloHandler.ts';
21+
22+
describe('EventBus', () => {
23+
it('should handle event', async () => {
24+
// mock handler 捕获调用参数
25+
const mockFn = async (msg: string) => {};
26+
mm(HelloHandler.prototype, 'handle', mockFn);
27+
28+
await app.mockModuleContextScope(async (ctx) => {
29+
const helloService = await ctx.getEggObject(HelloService);
30+
const eventWaiter = await app.getEventWaiter();
31+
32+
// 1. 先注册等待(必须在 emit 之前)
33+
const eventPromise = eventWaiter.await('helloEgg');
34+
35+
// 2. 触发业务逻辑(内部会 emit 事件)
36+
helloService.hello();
37+
38+
// 3. 等待 handler 执行完成
39+
await eventPromise;
40+
});
41+
42+
// 4. 验证 handler 被调用及参数
43+
assert.equal(mockFn.called, 1);
44+
assert.deepStrictEqual(mockFn.lastCalledArguments, ['hello']);
45+
});
46+
});
47+
```

0 commit comments

Comments
 (0)