@eggjs/skills 4.1.2-beta.5 → 4.1.2-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/egg/SKILL.md +93 -19
- package/egg-controller/SKILL.md +36 -6
- package/egg-controller/references/ajv-validate.md +146 -0
- package/egg-controller/references/middleware.md +133 -0
- package/egg-core/SKILL.md +70 -14
- package/egg-core/references/aop.md +219 -0
- package/egg-core/references/background-task.md +121 -0
- package/egg-core/references/eventbus.md +138 -0
- package/egg-unittest/SKILL.md +152 -0
- package/egg-unittest/references/background-task-test.md +55 -0
- package/egg-unittest/references/eventbus-test.md +47 -0
- package/egg-unittest/references/http-test.md +160 -0
- package/egg-unittest/references/mock.md +114 -0
- package/egg-unittest/references/service-test.md +75 -0
- package/package.json +5 -2
- package/PLAN.md +0 -396
|
@@ -0,0 +1,152 @@
|
|
|
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)` | 获取 SingletonProto / ContextProto 实例 |
|
|
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
|
+
| 代码写在 describe 内、hooks 外 | 放入 `beforeAll` / `beforeEach` | describe 体在加载阶段就执行 |
|
|
142
|
+
| `await app.ready()` 配合 bootstrap | 不需要 | bootstrap 自动处理生命周期 |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 参考资料
|
|
147
|
+
|
|
148
|
+
- `references/http-test.md` — HTTP 接口测试
|
|
149
|
+
- `references/service-test.md` — Service/DI 对象测试
|
|
150
|
+
- `references/mock.md` — Mock 模式
|
|
151
|
+
- `references/background-task-test.md` — BackgroundTaskHelper 测试
|
|
152
|
+
- `references/eventbus-test.md` — EventBus 测试
|
|
@@ -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
|
+
```
|
|
@@ -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,0 +1,160 @@
|
|
|
1
|
+
# HTTP 接口测试
|
|
2
|
+
|
|
3
|
+
## 常见错误
|
|
4
|
+
|
|
5
|
+
| 错误写法 | 正确写法 | 说明 |
|
|
6
|
+
| -------------------------------------------------------- | ------------------------------- | --------------------------------------------- |
|
|
7
|
+
| POST 测试不加 `app.mockCsrf()` | 在 POST 前调用 `app.mockCsrf()` | 安全插件默认开启 CSRF,不 mock 会返回 403 |
|
|
8
|
+
| `app.httpRequest().get('/').expect(200)` 不 return/await | 必须 `return` 或 `await` | 否则断言不会执行,测试永远通过 |
|
|
9
|
+
| `.expect({ foo: 'bar' })` 用于部分匹配 | 使用 `result.body` 手动断言 | `.expect(body)` 是全量匹配(deepStrictEqual) |
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 基本用法
|
|
14
|
+
|
|
15
|
+
通过 `app.httpRequest()` 发起 HTTP 请求,返回 SuperTest 对象:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { app } from '@eggjs/mock/bootstrap';
|
|
19
|
+
|
|
20
|
+
describe('UserController', () => {
|
|
21
|
+
it('should GET /api/users', () => {
|
|
22
|
+
return app.httpRequest()
|
|
23
|
+
.get('/api/users')
|
|
24
|
+
.expect(200)
|
|
25
|
+
.expect({ users: [] });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## POST 请求 + CSRF
|
|
33
|
+
|
|
34
|
+
POST/PUT/DELETE 请求需要先调用 `app.mockCsrf()` 跳过 CSRF 校验:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
it('should POST /api/users', () => {
|
|
38
|
+
app.mockCsrf();
|
|
39
|
+
return app.httpRequest()
|
|
40
|
+
.post('/api/users')
|
|
41
|
+
.send({ name: 'test', email: 'test@example.com' })
|
|
42
|
+
.expect(200)
|
|
43
|
+
.expect({ id: '1', name: 'test' });
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
表单提交使用 `.type('form')`:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
it('should POST form data', () => {
|
|
51
|
+
app.mockCsrf();
|
|
52
|
+
return app.httpRequest()
|
|
53
|
+
.post('/api/login')
|
|
54
|
+
.type('form')
|
|
55
|
+
.send({ username: 'admin', password: '123' })
|
|
56
|
+
.expect(200);
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 请求构造
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
app.httpRequest()
|
|
66
|
+
.get('/api/users')
|
|
67
|
+
.set('Authorization', 'Bearer token123') // 设置 header
|
|
68
|
+
.set('Accept', 'application/json') // 设置 Accept
|
|
69
|
+
.query({ page: 1, limit: 10 }) // 查询参数
|
|
70
|
+
.expect(200);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 响应断言
|
|
76
|
+
|
|
77
|
+
使用 `.expect()` 链式断言:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import assert from 'node:assert';
|
|
81
|
+
import { app } from '@eggjs/mock/bootstrap';
|
|
82
|
+
|
|
83
|
+
it('should validate response', () => {
|
|
84
|
+
return app.httpRequest()
|
|
85
|
+
.get('/api/users/1')
|
|
86
|
+
.expect(200) // 只校验状态码
|
|
87
|
+
.expect({ id: '1', name: 'test' }) // 只校验 body(deepStrictEqual)
|
|
88
|
+
.expect(200, { id: '1', name: 'test' }) // 状态码 + body 合并
|
|
89
|
+
.expect('hello world') // body 字符串匹配
|
|
90
|
+
.expect(/hello/) // body 正则匹配
|
|
91
|
+
.expect('content-type', /json/) // header 匹配
|
|
92
|
+
.expect([200, 302]) // 多状态码匹配(任一即可)
|
|
93
|
+
.expect(res => { // 自定义断言函数
|
|
94
|
+
assert(res.body.id);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
使用 `result` 做更灵活的断言:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import assert from 'node:assert';
|
|
103
|
+
import { app } from '@eggjs/mock/bootstrap';
|
|
104
|
+
|
|
105
|
+
it('should validate response', async () => {
|
|
106
|
+
const result = await app.httpRequest()
|
|
107
|
+
.get('/api/users/1');
|
|
108
|
+
|
|
109
|
+
assert.equal(result.status, 200);
|
|
110
|
+
assert.equal(result.body.name, 'test');
|
|
111
|
+
assert(result.body.id);
|
|
112
|
+
assert.match(result.headers['content-type'], /json/);
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
完整的请求构造和断言 API 可查看项目 node_modules 中 `@eggjs/supertest` 的类型定义。
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 端到端示例
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import assert from 'node:assert';
|
|
124
|
+
import { app } from '@eggjs/mock/bootstrap';
|
|
125
|
+
|
|
126
|
+
describe('test/controller/user.test.ts', () => {
|
|
127
|
+
describe('GET /api/users/:id', () => {
|
|
128
|
+
it('should return user', () => {
|
|
129
|
+
return app.httpRequest()
|
|
130
|
+
.get('/api/users/1')
|
|
131
|
+
.expect(200)
|
|
132
|
+
.expect({ id: '1', name: 'test' });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should return 404 when user not found', () => {
|
|
136
|
+
return app.httpRequest()
|
|
137
|
+
.get('/api/users/999')
|
|
138
|
+
.expect(404);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('POST /api/users', () => {
|
|
143
|
+
it('should create user', () => {
|
|
144
|
+
app.mockCsrf();
|
|
145
|
+
return app.httpRequest()
|
|
146
|
+
.post('/api/users')
|
|
147
|
+
.send({ name: 'new user', email: 'new@example.com' })
|
|
148
|
+
.expect(201);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return 422 with invalid params', () => {
|
|
152
|
+
app.mockCsrf();
|
|
153
|
+
return app.httpRequest()
|
|
154
|
+
.post('/api/users')
|
|
155
|
+
.send({ name: '' })
|
|
156
|
+
.expect(422);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Mock 模式
|
|
2
|
+
|
|
3
|
+
## 常见错误
|
|
4
|
+
|
|
5
|
+
| 错误写法 | 正确写法 | 说明 |
|
|
6
|
+
| ------------------------------ | ------------------------------------------ | ------------------------------ |
|
|
7
|
+
| `mm(service, 'method', fn)` | `mm(ServiceClass.prototype, 'method', fn)` | DI 对象需 mock 原型,不是实例 |
|
|
8
|
+
| 手动写 `afterEach(mm.restore)` | 不需要 | egg-bin 自动注入 mock 恢复 |
|
|
9
|
+
| `new Ajv()` mock 单独实例 | mock 原型方法 | DI 容器管理的对象通过原型 mock |
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## mm() — Mock Proto 方法
|
|
14
|
+
|
|
15
|
+
最常用的 mock 方式,mock DI 对象的原型方法:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import assert from 'node:assert';
|
|
19
|
+
import { app, mm } from '@eggjs/mock/bootstrap';
|
|
20
|
+
import { UserService } from '../app/modules/user/UserService.ts';
|
|
21
|
+
import { OrderService } from '../app/modules/order/OrderService.ts';
|
|
22
|
+
|
|
23
|
+
describe('OrderService', () => {
|
|
24
|
+
it('should mock user service', async () => {
|
|
25
|
+
mm(UserService.prototype, 'getById', async () => {
|
|
26
|
+
return { id: '1', name: 'mocked user' };
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const orderService = await app.getEggObject(OrderService);
|
|
30
|
+
const result = await orderService.createForUser('1');
|
|
31
|
+
assert.equal(result.userName, 'mocked user');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
mock 函数会自动记录调用信息,可以用来断言调用参数:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import assert from 'node:assert';
|
|
40
|
+
import { app, mm } from '@eggjs/mock/bootstrap';
|
|
41
|
+
import { NotifyService } from '../app/modules/notify/NotifyService.ts';
|
|
42
|
+
import { OrderService } from '../app/modules/order/OrderService.ts';
|
|
43
|
+
|
|
44
|
+
it('should call notify with correct args', async () => {
|
|
45
|
+
const mockFn = async (userId: string, message: string) => {};
|
|
46
|
+
mm(NotifyService.prototype, 'send', mockFn);
|
|
47
|
+
|
|
48
|
+
const orderService = await app.getEggObject(OrderService);
|
|
49
|
+
await orderService.create({ productId: '1' });
|
|
50
|
+
|
|
51
|
+
assert.equal(mockFn.called, 1); // 调用次数
|
|
52
|
+
assert.deepStrictEqual(mockFn.lastCalledArguments, ['user-1', '订单创建成功']); // 最后一次调用参数
|
|
53
|
+
// mockFn.calledArguments — 所有调用参数的数组
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## mm.spy() — 不替换实现,只记录调用
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
it('should spy on method', async () => {
|
|
63
|
+
mm.spy(NotifyService.prototype, 'send');
|
|
64
|
+
|
|
65
|
+
const orderService = await app.getEggObject(OrderService);
|
|
66
|
+
await orderService.create({ productId: '1' });
|
|
67
|
+
|
|
68
|
+
// 原方法正常执行,同时记录了调用信息
|
|
69
|
+
const sendFn = NotifyService.prototype.send;
|
|
70
|
+
assert.equal(sendFn.called, 1);
|
|
71
|
+
assert.equal(sendFn.lastCalledArguments[0], 'user-1');
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## app.mockHttpclient() — Mock HttpClient 请求
|
|
78
|
+
|
|
79
|
+
Mock 通过 `@Inject() httpclient: HttpClient` 注入的 HttpClient 发送的请求:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
it('should mock external API', () => {
|
|
83
|
+
app.mockHttpclient('https://api.example.com/users', {
|
|
84
|
+
data: JSON.stringify({ name: 'test' }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return app.httpRequest()
|
|
88
|
+
.get('/api/proxy/users')
|
|
89
|
+
.expect(200)
|
|
90
|
+
.expect({ name: 'test' });
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## app.mockCsrf() — 跳过 CSRF
|
|
97
|
+
|
|
98
|
+
POST/PUT/DELETE 测试时跳过 CSRF 校验:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
it('should POST without CSRF error', () => {
|
|
102
|
+
app.mockCsrf();
|
|
103
|
+
return app.httpRequest()
|
|
104
|
+
.post('/api/users')
|
|
105
|
+
.send({ name: 'test' })
|
|
106
|
+
.expect(200);
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Mock 恢复
|
|
113
|
+
|
|
114
|
+
egg-bin 自动注入 `@eggjs/mock/setup_vitest`,会在 `afterEach` 钩子中自动调用 `mm.restore()`,无需手动编写。
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Service / DI 对象测试
|
|
2
|
+
|
|
3
|
+
## 常见错误
|
|
4
|
+
|
|
5
|
+
| 错误写法 | 正确写法 | 说明 |
|
|
6
|
+
| ------------------------ | ----------------------------------------- | ------------------- |
|
|
7
|
+
| `ctx.service.user.get()` | `ctx.getEggObject(UserService)` | 旧写法,新项目用 DI |
|
|
8
|
+
| 不 await `getEggObject` | `const svc = await ctx.getEggObject(Svc)` | 返回 Promise |
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Singleton 测试
|
|
13
|
+
|
|
14
|
+
`@SingletonProto` 对象直接通过 `app.getEggObject()` 获取:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import assert from 'node:assert';
|
|
18
|
+
import { app } from '@eggjs/mock/bootstrap';
|
|
19
|
+
import { ConfigService } from '../app/modules/foo/ConfigService.ts';
|
|
20
|
+
|
|
21
|
+
describe('ConfigService', () => {
|
|
22
|
+
it('should get config', async () => {
|
|
23
|
+
const configService = await app.getEggObject(ConfigService);
|
|
24
|
+
const value = configService.get('key');
|
|
25
|
+
assert.equal(value, 'expected');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## ContextProto 测试
|
|
33
|
+
|
|
34
|
+
`@ContextProto` 对象可以直接通过 `app.getEggObject()` 获取,也可以在 `app.mockModuleContextScope` 中通过 `ctx.getEggObject()` 获取。后者会创建带 DI 生命周期的 ctx,退出时自动销毁:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import assert from 'node:assert';
|
|
38
|
+
import { app } from '@eggjs/mock/bootstrap';
|
|
39
|
+
import { UserService } from '../app/modules/user/UserService.ts';
|
|
40
|
+
|
|
41
|
+
describe('UserService', () => {
|
|
42
|
+
it('should get user in context scope', async () => {
|
|
43
|
+
await app.mockModuleContextScope(async (ctx) => {
|
|
44
|
+
const userService = await ctx.getEggObject(UserService);
|
|
45
|
+
const user = await userService.getById('1');
|
|
46
|
+
assert(user);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Mock 被注入的依赖
|
|
55
|
+
|
|
56
|
+
当 Service A 依赖 Service B 时,mock B 的原型方法:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import assert from 'node:assert';
|
|
60
|
+
import { app, mm } from '@eggjs/mock/bootstrap';
|
|
61
|
+
import { OrderService } from '../app/modules/order/OrderService.ts';
|
|
62
|
+
import { PaymentService } from '../app/modules/payment/PaymentService.ts';
|
|
63
|
+
|
|
64
|
+
describe('OrderService', () => {
|
|
65
|
+
it('should create order with mocked payment', async () => {
|
|
66
|
+
mm(PaymentService.prototype, 'charge', async () => {
|
|
67
|
+
return { transactionId: 'mock-tx-001' };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const orderService = await app.getEggObject(OrderService);
|
|
71
|
+
const order = await orderService.create({ productId: '1', amount: 100 });
|
|
72
|
+
assert.equal(order.transactionId, 'mock-tx-001');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eggjs/skills",
|
|
3
|
-
"version": "4.1.2-beta.
|
|
3
|
+
"version": "4.1.2-beta.6",
|
|
4
4
|
"description": "agent skills for egg",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"egg",
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
"directory": "packages/skills"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"
|
|
21
|
+
"egg-controller/**/*.md",
|
|
22
|
+
"egg-core/**/*.md",
|
|
23
|
+
"egg-unittest/**/*.md",
|
|
24
|
+
"egg/**/*.md"
|
|
22
25
|
],
|
|
23
26
|
"publishConfig": {
|
|
24
27
|
"access": "public"
|