@eggjs/skills 0.0.0 → 4.1.2-beta.10

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.
@@ -0,0 +1,181 @@
1
+ # Proto 开发指南
2
+
3
+ ## SingletonProto vs ContextProto
4
+
5
+ | | SingletonProto | ContextProto |
6
+ | -------- | ------------------ | ------------------ |
7
+ | 创建时机 | 应用启动时立即创建 | 请求到达时按需创建 |
8
+ | 实例数量 | 整个应用 1 个 | 每个请求 1 个 |
9
+ | 销毁时机 | 应用关闭 | 请求结束 |
10
+
11
+ **决策逻辑:默认使用 `@SingletonProto()`**,性能更好。只有当需要在多个服务间共享请求级隔离状态时,才使用 `@ContextProto()`。
12
+
13
+ ```typescript
14
+ import { SingletonProto, ContextProto } from 'egg';
15
+
16
+ // ✅ 默认选择:无状态服务用 SingletonProto
17
+ @SingletonProto()
18
+ export class UserService {
19
+ async findUser(id: string): Promise<User> {
20
+ // 无需请求级状态,适合 Singleton
21
+ }
22
+ }
23
+
24
+ // 仅在需要请求级隔离时使用 ContextProto
25
+ @ContextProto()
26
+ export class RequestContext {
27
+ traceId: string;
28
+ userId: string;
29
+ // 每个请求独立的状态
30
+ }
31
+ ```
32
+
33
+ ## 装饰器参数
34
+
35
+ `@SingletonProto()` 和 `@ContextProto()` 接受相同的可选参数:
36
+
37
+ | 参数 | 类型 | 默认值 | 说明 |
38
+ | ------------- | ----------- | -------------- | ------------------------------------ |
39
+ | name | string | 类名首字母小写 | 实例名称 |
40
+ | accessLevel | AccessLevel | PRIVATE | 跨模块可见性 |
41
+ | protoImplType | string | 'DEFAULT' | 实现类型标记(高级用法,一般不需要) |
42
+
43
+ 名称推导规则:类名首字母自动转小写,如 `MyService` → `myService`。
44
+
45
+ ```typescript
46
+ import { SingletonProto, AccessLevel } from 'egg';
47
+
48
+ // 使用默认参数:name 为 "helloService",accessLevel 为 PRIVATE
49
+ @SingletonProto()
50
+ export class HelloService {}
51
+
52
+ // 自定义参数
53
+ @SingletonProto({
54
+ name: 'customName',
55
+ accessLevel: AccessLevel.PUBLIC,
56
+ })
57
+ export class HelloService {}
58
+ ```
59
+
60
+ ## 注入规则
61
+
62
+ **SingletonProto 可以注入 ContextProto**,框架会自动处理生命周期差异。
63
+
64
+ 实现机制:框架不会直接注入 ContextProto 实例,而是通过 lazy getter/Proxy,在每次访问时从当前请求上下文动态解析,确保每个请求拿到各自的实例。
65
+
66
+ ```typescript
67
+ import { SingletonProto, ContextProto, Inject } from 'egg';
68
+
69
+ @ContextProto()
70
+ export class RequestContext {
71
+ userId: string;
72
+ }
73
+
74
+ @SingletonProto()
75
+ export class AppService {
76
+ @Inject()
77
+ requestContext: RequestContext; // ✅ 每次访问自动解析当前请求的实例
78
+ }
79
+ ```
80
+
81
+ ContextProto 也可以注入 SingletonProto:
82
+
83
+ ```typescript
84
+ @ContextProto()
85
+ export class RequestHandler {
86
+ @Inject()
87
+ appService: AppService; // ✅ 直接注入
88
+ }
89
+ ```
90
+
91
+ ## AccessLevel 跨模块访问
92
+
93
+ - `AccessLevel.PRIVATE`(默认):仅同模块内可注入
94
+ - `AccessLevel.PUBLIC`:跨模块可注入
95
+
96
+ 模块边界由 `package.json` 中的 `eggModule.name` 决定。
97
+
98
+ ```typescript
99
+ // userModule 中
100
+ @SingletonProto({ accessLevel: AccessLevel.PUBLIC })
101
+ export class UserService {}
102
+
103
+ // orderModule 中
104
+ @SingletonProto()
105
+ export class OrderService {
106
+ @Inject()
107
+ userService: UserService; // ✅ UserService 是 PUBLIC,跨模块可注入
108
+ }
109
+ ```
110
+
111
+ ```typescript
112
+ // userModule 中
113
+ @SingletonProto() // 默认 PRIVATE
114
+ export class UserHelper {}
115
+
116
+ // orderModule 中
117
+ @SingletonProto()
118
+ export class OrderService {
119
+ @Inject()
120
+ userHelper: UserHelper; // ❌ 报错:UserHelper 是 PRIVATE,不能跨模块注入
121
+ }
122
+ ```
123
+
124
+ ## 生命周期
125
+
126
+ Proto 对象支持以下生命周期钩子,按执行顺序排列:
127
+
128
+ | 阶段 | 装饰器 | 说明 |
129
+ | ---- | --------------------------- | -------------------------------- |
130
+ | 1 | constructor | 构造函数 |
131
+ | 2 | `@LifecyclePostConstruct()` | 构造完成后 |
132
+ | 3 | `@LifecyclePreInject()` | 注入前 |
133
+ | 4 | — | 依赖注入 |
134
+ | 5 | `@LifecyclePostInject()` | 注入完成后 |
135
+ | 6 | `@LifecycleInit()` | 异步初始化(对象就绪前最后一步) |
136
+ | 7 | — | **对象就绪,可被使用** |
137
+ | 8 | `@LifecyclePreDestroy()` | 销毁前 |
138
+ | 9 | `@LifecycleDestroy()` | 销毁,用于清理资源 |
139
+
140
+ 常用的是 `@LifecycleInit()` 和 `@LifecycleDestroy()`。
141
+
142
+ **示例 1:SingletonProto 异步初始化和资源清理**
143
+
144
+ ```typescript
145
+ import { SingletonProto, LifecycleInit, LifecycleDestroy } from 'egg';
146
+
147
+ @SingletonProto()
148
+ export class DatabaseService {
149
+ private connection: Connection;
150
+
151
+ @LifecycleInit()
152
+ async init(): Promise<void> {
153
+ this.connection = await createConnection();
154
+ }
155
+
156
+ @LifecycleDestroy()
157
+ async destroy(): Promise<void> {
158
+ await this.connection.close();
159
+ }
160
+ }
161
+ ```
162
+
163
+ **示例 2:ContextProto 在注入完成后初始化状态**
164
+
165
+ ```typescript
166
+ import { ContextProto, Inject, LifecyclePostInject } from 'egg';
167
+
168
+ @ContextProto()
169
+ export class RequestTracer {
170
+ @Inject()
171
+ traceService: TraceService;
172
+
173
+ traceId: string;
174
+
175
+ @LifecyclePostInject()
176
+ async postInject(): Promise<void> {
177
+ // 依赖注入完成后,利用注入的服务初始化状态
178
+ this.traceId = await this.traceService.generateTraceId();
179
+ }
180
+ }
181
+ ```
@@ -0,0 +1,149 @@
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().get('/').expect(200).expect('hello world');
69
+ });
70
+ });
71
+ ```
72
+
73
+ ### Service 测试
74
+
75
+ ```typescript
76
+ import assert from 'node:assert';
77
+ import { app } from '@eggjs/mock/bootstrap';
78
+ import { UserService } from '../app/modules/user/UserService.ts';
79
+
80
+ describe('test/service/user.test.ts', () => {
81
+ it('should get user', async () => {
82
+ const userService = await app.getEggObject(UserService);
83
+ const user = await userService.getById('1');
84
+ assert(user);
85
+ assert.equal(user.name, 'test');
86
+ });
87
+ });
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 测试场景决策树
93
+
94
+ ```
95
+ 要测什么?
96
+
97
+ 1. HTTP 接口(GET/POST/PUT/DELETE)?
98
+ → 参考 references/http-test.md
99
+
100
+ 2. Service / DI 对象的方法?
101
+ → 参考 references/service-test.md
102
+
103
+ 3. 需要 mock 外部依赖?(HTTP 调用、Service 方法、Session、CSRF)
104
+ → 参考 references/mock.md
105
+
106
+ 4. BackgroundTaskHelper(后台异步任务)?
107
+ → 参考 references/background-task-test.md
108
+
109
+ 5. EventBus(事件驱动)?
110
+ → 参考 references/eventbus-test.md
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 快速参考
116
+
117
+ | API | 说明 |
118
+ | ---------------------------------------------------- | --------------------------------------- |
119
+ | `import { app, mm } from '@eggjs/mock/bootstrap'` | 标准测试入口 |
120
+ | `app.httpRequest().get('/path').expect(200)` | HTTP 接口测试 |
121
+ | `app.getEggObject(Class)` | 获取 SingletonProto / ContextProto 实例 |
122
+ | `app.mockModuleContextScope(async (ctx) => { ... })` | ContextProto 测试作用域 |
123
+ | `mm(Class.prototype, 'method', fn)` | Mock Proto 方法 |
124
+ | `app.mockCsrf()` | 跳过 CSRF 校验(POST 测试必备) |
125
+ | `app.mockHttpclient(url, data)` | Mock 外部 HTTP 调用 |
126
+ | `app.getEventWaiter()` | 获取 EventBus 事件等待器 |
127
+
128
+ ---
129
+
130
+ ## 常见错误
131
+
132
+ | 错误写法 | 正确写法 | 说明 |
133
+ | ---------------------------------- | --------------------------------------------- | --------------------------- |
134
+ | `import { app } from 'egg'` | `import { app } from '@eggjs/mock/bootstrap'` | 测试使用 mock 包 |
135
+ | `before()` / `after()` | `beforeAll()` / `afterAll()` | Vitest 钩子,不是 Mocha |
136
+ | POST 测试报 403 | 加 `app.mockCsrf()` | 安全插件默认开启 CSRF |
137
+ | 手动写 `afterEach(mm.restore)` | 不需要 | egg-bin 自动注入 mock 恢复 |
138
+ | 代码写在 describe 内、hooks 外 | 放入 `beforeAll` / `beforeEach` | describe 体在加载阶段就执行 |
139
+ | `await app.ready()` 配合 bootstrap | 不需要 | bootstrap 自动处理生命周期 |
140
+
141
+ ---
142
+
143
+ ## 参考资料
144
+
145
+ - `references/http-test.md` — HTTP 接口测试
146
+ - `references/service-test.md` — Service/DI 对象测试
147
+ - `references/mock.md` — Mock 模式
148
+ - `references/background-task-test.md` — BackgroundTaskHelper 测试
149
+ - `references/eventbus-test.md` — EventBus 测试
@@ -0,0 +1,53 @@
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().get('/api/trigger-task').expect(200);
46
+
47
+ // 等待后台任务完成
48
+ await app.backgroundTasksFinished();
49
+
50
+ const countService = await app.getEggObject(CountService);
51
+ assert.equal(countService.count, 1);
52
+ });
53
+ ```
@@ -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,145 @@
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().get('/api/users').expect(200).expect({ users: [] });
23
+ });
24
+ });
25
+ ```
26
+
27
+ ---
28
+
29
+ ## POST 请求 + CSRF
30
+
31
+ POST/PUT/DELETE 请求需要先调用 `app.mockCsrf()` 跳过 CSRF 校验:
32
+
33
+ ```typescript
34
+ it('should POST /api/users', () => {
35
+ app.mockCsrf();
36
+ return app
37
+ .httpRequest()
38
+ .post('/api/users')
39
+ .send({ name: 'test', email: 'test@example.com' })
40
+ .expect(200)
41
+ .expect({ id: '1', name: 'test' });
42
+ });
43
+ ```
44
+
45
+ 表单提交使用 `.type('form')`:
46
+
47
+ ```typescript
48
+ it('should POST form data', () => {
49
+ app.mockCsrf();
50
+ return app.httpRequest().post('/api/login').type('form').send({ username: 'admin', password: '123' }).expect(200);
51
+ });
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 请求构造
57
+
58
+ ```typescript
59
+ app
60
+ .httpRequest()
61
+ .get('/api/users')
62
+ .set('Authorization', 'Bearer token123') // 设置 header
63
+ .set('Accept', 'application/json') // 设置 Accept
64
+ .query({ page: 1, limit: 10 }) // 查询参数
65
+ .expect(200);
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 响应断言
71
+
72
+ 使用 `.expect()` 链式断言:
73
+
74
+ ```typescript
75
+ import assert from 'node:assert';
76
+ import { app } from '@eggjs/mock/bootstrap';
77
+
78
+ it('should validate response', () => {
79
+ return app
80
+ .httpRequest()
81
+ .get('/api/users/1')
82
+ .expect(200) // 只校验状态码
83
+ .expect({ id: '1', name: 'test' }) // 只校验 body(deepStrictEqual)
84
+ .expect(200, { id: '1', name: 'test' }) // 状态码 + body 合并
85
+ .expect('hello world') // body 字符串匹配
86
+ .expect(/hello/) // body 正则匹配
87
+ .expect('content-type', /json/) // header 匹配
88
+ .expect([200, 302]) // 多状态码匹配(任一即可)
89
+ .expect((res) => {
90
+ // 自定义断言函数
91
+ assert(res.body.id);
92
+ });
93
+ });
94
+ ```
95
+
96
+ 使用 `result` 做更灵活的断言:
97
+
98
+ ```typescript
99
+ import assert from 'node:assert';
100
+ import { app } from '@eggjs/mock/bootstrap';
101
+
102
+ it('should validate response', async () => {
103
+ const result = await app.httpRequest().get('/api/users/1');
104
+
105
+ assert.equal(result.status, 200);
106
+ assert.equal(result.body.name, 'test');
107
+ assert(result.body.id);
108
+ assert.match(result.headers['content-type'], /json/);
109
+ });
110
+ ```
111
+
112
+ 完整的请求构造和断言 API 可查看项目 node_modules 中 `@eggjs/supertest` 的类型定义。
113
+
114
+ ---
115
+
116
+ ## 端到端示例
117
+
118
+ ```typescript
119
+ import assert from 'node:assert';
120
+ import { app } from '@eggjs/mock/bootstrap';
121
+
122
+ describe('test/controller/user.test.ts', () => {
123
+ describe('GET /api/users/:id', () => {
124
+ it('should return user', () => {
125
+ return app.httpRequest().get('/api/users/1').expect(200).expect({ id: '1', name: 'test' });
126
+ });
127
+
128
+ it('should return 404 when user not found', () => {
129
+ return app.httpRequest().get('/api/users/999').expect(404);
130
+ });
131
+ });
132
+
133
+ describe('POST /api/users', () => {
134
+ it('should create user', () => {
135
+ app.mockCsrf();
136
+ return app.httpRequest().post('/api/users').send({ name: 'new user', email: 'new@example.com' }).expect(201);
137
+ });
138
+
139
+ it('should return 422 with invalid params', () => {
140
+ app.mockCsrf();
141
+ return app.httpRequest().post('/api/users').send({ name: '' }).expect(422);
142
+ });
143
+ });
144
+ });
145
+ ```
@@ -0,0 +1,108 @@
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().get('/api/proxy/users').expect(200).expect({ name: 'test' });
88
+ });
89
+ ```
90
+
91
+ ---
92
+
93
+ ## app.mockCsrf() — 跳过 CSRF
94
+
95
+ POST/PUT/DELETE 测试时跳过 CSRF 校验:
96
+
97
+ ```typescript
98
+ it('should POST without CSRF error', () => {
99
+ app.mockCsrf();
100
+ return app.httpRequest().post('/api/users').send({ name: 'test' }).expect(200);
101
+ });
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Mock 恢复
107
+
108
+ egg-bin 自动注入 `@eggjs/mock/setup_vitest`,会在 `afterEach` 钩子中自动调用 `mm.restore()`,无需手动编写。