1 | # egg-mock
|
2 |
|
3 | [![NPM version][npm-image]][npm-url]
|
4 | [![Node.js CI](https://github.com/eggjs/egg-mock/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/egg-mock/actions/workflows/nodejs.yml)
|
5 | [![Test coverage][codecov-image]][codecov-url]
|
6 | [![npm download][download-image]][download-url]
|
7 |
|
8 | [npm-image]: https://img.shields.io/npm/v/egg-mock.svg?style=flat-square
|
9 | [npm-url]: https://npmjs.org/package/egg-mock
|
10 | [codecov-image]: https://codecov.io/github/eggjs/egg-mock/coverage.svg?branch=master
|
11 | [codecov-url]: https://codecov.io/github/eggjs/egg-mock?branch=master
|
12 | [download-image]: https://img.shields.io/npm/dm/egg-mock.svg?style=flat-square
|
13 | [download-url]: https://npmjs.org/package/egg-mock
|
14 |
|
15 | 一个数据模拟的库,更方便地测试 Egg 应用、插件及自定义 Egg 框架。`egg-mock` 拓展自 [node_modules/mm](https://github.com/node-modules/mm),你可以使用所有 `mm` 包含的 API。
|
16 |
|
17 | ## Install
|
18 |
|
19 | ```bash
|
20 | $ npm i egg-mock --save-dev
|
21 | ```
|
22 |
|
23 | ## Usage
|
24 |
|
25 | ### 创建测试用例
|
26 |
|
27 | 通过 `mm.app` 启动应用,可以使用 App 的 API 模拟数据
|
28 |
|
29 | ```js
|
30 | // test/index.test.js
|
31 | const path = require('path');
|
32 | const mm = require('egg-mock');
|
33 |
|
34 | describe('some test', () => {
|
35 | let app;
|
36 | before(() => {
|
37 | app = mm.app({
|
38 | baseDir: 'apps/foo'
|
39 | customEgg: path.join(__dirname, '../node_modules/egg'),
|
40 | });
|
41 | return app.ready();
|
42 | })
|
43 | after(() => app.close());
|
44 |
|
45 | it('should request /', () => {
|
46 | return app.httpRequest()
|
47 | .get('/')
|
48 | .expect(200);
|
49 | });
|
50 | });
|
51 | ```
|
52 |
|
53 | 使用 `mm.app` 启动后可以通过 `app.agent` 访问到 agent 对象。
|
54 |
|
55 | 使用 `mm.cluster` 启动多进程测试,API 与 `mm.app` 一致。
|
56 |
|
57 | ### 应用开发者
|
58 |
|
59 | 应用开发者不需要传入 baseDir,其为当前路径
|
60 |
|
61 | ```js
|
62 | before(() => {
|
63 | app = mm.app({
|
64 | customEgg: path.join(__dirname, '../node_modules/egg'),
|
65 | });
|
66 | return app.ready();
|
67 | });
|
68 | ```
|
69 |
|
70 | ### 框架开发者
|
71 |
|
72 | 框架开发者需要指定 customEgg,会将当前路径指定为框架入口
|
73 |
|
74 | ```js
|
75 | before(() => {
|
76 | app = mm.app({
|
77 | baseDir: 'apps/demo',
|
78 | customEgg: true,
|
79 | });
|
80 | return app.ready();
|
81 | });
|
82 | ```
|
83 |
|
84 | ### 插件开发者
|
85 |
|
86 | 在插件目录下执行测试用例时,只要 `package.json` 中有 `eggPlugin.name` 字段,就会自动把当前目录加到插件列表中。
|
87 |
|
88 | ```js
|
89 | before(() => {
|
90 | app = mm.app({
|
91 | baseDir: 'apps/demo',
|
92 | customEgg: path.join(__dirname, '../node_modules/egg'),
|
93 | });
|
94 | return app.ready();
|
95 | });
|
96 | ```
|
97 |
|
98 | 也可以通过 customEgg 指定其他框架,比如希望在 aliyun-egg 和 framework-b 同时测试此插件。
|
99 |
|
100 | ```js
|
101 | describe('aliyun-egg', () => {
|
102 | let app;
|
103 | before(() => {
|
104 | app = mm.app({
|
105 | baseDir: 'apps/demo',
|
106 | customEgg: path.join(__dirname, 'node_modules/aliyun-egg'),
|
107 | });
|
108 | return app.ready();
|
109 | });
|
110 | });
|
111 |
|
112 | describe('framework-b', () => {
|
113 | let app;
|
114 | before(() => {
|
115 | app = mm.app({
|
116 | baseDir: 'apps/demo',
|
117 | customEgg: path.join(__dirname, 'node_modules/framework-b'),
|
118 | });
|
119 | return app.ready();
|
120 | });
|
121 | });
|
122 | ```
|
123 |
|
124 | 如果当前目录确实是一个 egg 插件,但是又不想当它是一个插件来测试,可以通过 `options.plugin` 选项来关闭:
|
125 |
|
126 | ```js
|
127 | before(() => {
|
128 | app = mm.app({
|
129 | baseDir: 'apps/demo',
|
130 | customEgg: path.join(__dirname, 'node_modules/egg'),
|
131 | plugin: false,
|
132 | });
|
133 | return app.ready();
|
134 | });
|
135 | ```
|
136 |
|
137 | ## API
|
138 |
|
139 | ### mm.app(options)
|
140 |
|
141 | 创建一个 mock 的应用。
|
142 |
|
143 | ### mm.cluster(options)
|
144 |
|
145 | 创建一个多进程应用,因为是多进程应用,无法获取 worker 的属性,只能通过 supertest 请求。
|
146 |
|
147 | ```js
|
148 | const mm = require('egg-mock');
|
149 | describe('test/app.js', () => {
|
150 | let app, config;
|
151 | before(() => {
|
152 | app = mm.cluster();
|
153 | return app.ready();
|
154 | });
|
155 | after(() => app.close());
|
156 |
|
157 | it('some test', () => {
|
158 | return app.httpRequest()
|
159 | .get('/config')
|
160 | .expect(200)
|
161 | });
|
162 | });
|
163 | ```
|
164 |
|
165 | 默认会启用覆盖率,因为覆盖率比较慢,可以设置 coverage 关闭
|
166 |
|
167 | ```js
|
168 | mm.cluster({
|
169 | coverage: false,
|
170 | });
|
171 | ```
|
172 |
|
173 | ### mm.env(env)
|
174 |
|
175 | 设置环境变量,主要用于启动阶段,运行阶段可以使用 app.mockEnv。
|
176 |
|
177 | ```js
|
178 | // 模拟生成环境
|
179 | mm.env('prod');
|
180 | mm.app({
|
181 | cache: false,
|
182 | });
|
183 | ```
|
184 |
|
185 | 具体值见 <https://github.com/eggjs/egg-core/blob/master/lib/loader/egg_loader.js#L82>
|
186 |
|
187 | ### mm.consoleLevel(level)
|
188 |
|
189 | mock 终端日志打印级别
|
190 |
|
191 | ```js
|
192 | // 不输出到终端
|
193 | mm.consoleLevel('NONE');
|
194 | ```
|
195 |
|
196 | 可选 level 为 `DEBUG`, `INFO`, `WARN`, `ERROR`, `NONE`
|
197 |
|
198 | ### mm.home(homePath)
|
199 |
|
200 | 模拟操作系统用户目录
|
201 |
|
202 | ### mm.restore
|
203 |
|
204 | 还原所有 mock 数据,一般需要结合 `afterEach(mm.restore)` 使用
|
205 |
|
206 | ### options
|
207 |
|
208 | mm.app 和 mm.cluster 的配置参数
|
209 |
|
210 | #### baseDir {String}
|
211 |
|
212 | 当前应用的目录,如果是应用本身的测试可以不填默认为 $CWD。
|
213 |
|
214 | 指定完整路径
|
215 |
|
216 | ```js
|
217 | mm.app({
|
218 | baseDir: path.join(__dirname, 'fixtures/apps/demo'),
|
219 | })
|
220 | ```
|
221 |
|
222 | 也支持缩写,找 test/fixtures 目录下的
|
223 |
|
224 | ```js
|
225 | mm.app({
|
226 | baseDir: 'apps/demo',
|
227 | })
|
228 | ```
|
229 |
|
230 | #### customEgg {String/Boolean}
|
231 |
|
232 | 指定框架路径
|
233 |
|
234 | ```js
|
235 | mm.app({
|
236 | baseDir: 'apps/demo',
|
237 | customEgg: path.join(__dirname, 'fixtures/egg'),
|
238 | })
|
239 | ```
|
240 |
|
241 | 对于框架的测试用例,可以指定 true,会自动加载当前路径。
|
242 |
|
243 | #### plugin
|
244 |
|
245 | 指定插件的路径,只用于插件测试。设置为 true 会将当前路径设置到插件列表。
|
246 |
|
247 | ```js
|
248 | mm.app({
|
249 | baseDir: 'apps/demo',
|
250 | plugin: true,
|
251 | })
|
252 | ```
|
253 |
|
254 | #### plugins {Object}
|
255 |
|
256 | 传入插件列表,可以自定义多个插件
|
257 |
|
258 | #### cache {Boolean}
|
259 |
|
260 | 是否需要缓存,默认开启。
|
261 |
|
262 | 是通过 baseDir 缓存的,如果不需要可以关闭,但速度会慢。
|
263 |
|
264 | #### clean {Boolean}
|
265 |
|
266 | 是否需要清理 log 目录,默认开启。
|
267 |
|
268 | 如果是通过 ava 等并行测试框架进行测试,需要手动在执行测试前进行统一的日志清理,不能通过 mm 来处理,设置 `clean` 为 `false`。
|
269 |
|
270 | ### app.mockLog([logger]) and app.expectLog(str[, logger]), app.notExpectLog(str[, logger])
|
271 |
|
272 | 断言指定的字符串记录在指定的日志中。
|
273 | 建议 `app.mockLog()` 和 `app.expectLog()` 或者 `app.notExpectLog()` 配对使用。
|
274 | 单独使用 `app.expectLog()` 或者 `app.notExpectLog()` 需要依赖日志的写入速度,在服务器磁盘高 IO 的时候,会出现不稳定的结果。
|
275 |
|
276 | ```js
|
277 | it('should work', async () => {
|
278 | // 将日志记录到内存,用于下面的 expectLog
|
279 | app.mockLog();
|
280 | await app.httpRequest()
|
281 | .get('/')
|
282 | .expect('hello world')
|
283 | .expect(200);
|
284 |
|
285 | app.expectLog('foo in logger');
|
286 | app.expectLog('foo in coreLogger', 'coreLogger');
|
287 | app.expectLog('foo in myCustomLogger', 'myCustomLogger');
|
288 |
|
289 | app.notExpectLog('bar in logger');
|
290 | app.notExpectLog('bar in coreLogger', 'coreLogger');
|
291 | app.notExpectLog('bar in myCustomLogger', 'myCustomLogger');
|
292 | });
|
293 | ```
|
294 |
|
295 | ### app.httpRequest()
|
296 |
|
297 | 请求当前应用 http 服务的辅助工具。
|
298 |
|
299 | ```js
|
300 | it('should work', () => {
|
301 | return app.httpRequest()
|
302 | .get('/')
|
303 | .expect('hello world')
|
304 | .expect(200);
|
305 | });
|
306 | ```
|
307 |
|
308 | 更多信息请查看 [supertest](https://github.com/visionmedia/supertest) 的 API 说明。
|
309 |
|
310 | #### .unexpectHeader(name)
|
311 |
|
312 | 断言当前请求响应不包含指定 header
|
313 |
|
314 | ```js
|
315 | it('should work', () => {
|
316 | return app.httpRequest()
|
317 | .get('/')
|
318 | .unexpectHeader('set-cookie')
|
319 | .expect(200);
|
320 | });
|
321 | ```
|
322 |
|
323 | #### .expectHeader(name)
|
324 |
|
325 | 断言当前请求响应包含指定 header
|
326 |
|
327 | ```js
|
328 | it('should work', () => {
|
329 | return app.httpRequest()
|
330 | .get('/')
|
331 | .expectHeader('set-cookie')
|
332 | .expect(200);
|
333 | });
|
334 | ```
|
335 |
|
336 | ### app.mockContext(options)
|
337 |
|
338 | 模拟上下文数据
|
339 |
|
340 | ```js
|
341 | const ctx = app.mockContext({
|
342 | user: {
|
343 | name: 'Jason'
|
344 | }
|
345 | });
|
346 | console.log(ctx.user.name); // Jason
|
347 | ```
|
348 |
|
349 |
|
350 | ### app.mockContextScope(fn, options)
|
351 |
|
352 | 安全的模拟上下文数据,同一用例用多次调用 mockContext 可能会造成 AsyncLocalStorage 污染
|
353 |
|
354 | ```js
|
355 | await app.mockContextScope(async ctx => {
|
356 | console.log(ctx.user.name); // Jason
|
357 | }, {
|
358 | user: {
|
359 | name: 'Jason'
|
360 | }
|
361 | });
|
362 | ```
|
363 |
|
364 | ### app.mockCookies(data)
|
365 |
|
366 | ```js
|
367 | app.mockCookies({
|
368 | foo: 'bar'
|
369 | });
|
370 | const ctx = app.mockContext();
|
371 | console.log(ctx.getCookie('foo'));
|
372 | ```
|
373 |
|
374 | ### app.mockHeaders(data)
|
375 |
|
376 | 模拟请求头
|
377 |
|
378 | ### app.mockSession(data)
|
379 |
|
380 | ```js
|
381 | app.mockSession({
|
382 | foo: 'bar'
|
383 | });
|
384 | const ctx = app.mockContext();
|
385 | console.log(ctx.session.foo);
|
386 | ```
|
387 |
|
388 | ### app.mockService(service, methodName, fn)
|
389 |
|
390 | ```js
|
391 | it('should mock user name', function* () {
|
392 | app.mockService('user', 'getName', function* (ctx, methodName, args) {
|
393 | return 'popomore';
|
394 | });
|
395 | const ctx = app.mockContext();
|
396 | yield ctx.service.user.getName();
|
397 | });
|
398 | ```
|
399 |
|
400 | ### app.mockServiceError(service, methodName, error)
|
401 |
|
402 | 可以模拟一个错误
|
403 |
|
404 | ```js
|
405 | app.mockServiceError('user', 'home', new Error('mock error'));
|
406 | ```
|
407 |
|
408 | ### app.mockCsrf()
|
409 |
|
410 | 模拟 csrf,不用传递 token
|
411 |
|
412 | ```js
|
413 | app.mockCsrf();
|
414 |
|
415 | return app.httpRequest()
|
416 | .post('/login')
|
417 | .expect(302);
|
418 | ```
|
419 |
|
420 | ### app.mockHttpclient(url, method, data)
|
421 |
|
422 | 模拟 httpclient 的请求,例如 `ctx.curl`
|
423 |
|
424 | ```js
|
425 | app.get('/', async ctx => {
|
426 | const ret = await ctx.curl('https://eggjs.org');
|
427 | this.body = ret.data.toString();
|
428 | });
|
429 |
|
430 | app.mockHttpclient('https://eggjs.org', {
|
431 | // 模拟的参数,可以是 buffer / string / json / function
|
432 | // 都会转换成 buffer
|
433 | // 按照请求时的 options.dataType 来做对应的转换
|
434 | data: 'mock egg',
|
435 | });
|
436 |
|
437 | return app.httpRequest()
|
438 | .post('/')
|
439 | .expect('mock egg');
|
440 | ```
|
441 |
|
442 | ## Bootstrap
|
443 |
|
444 | 我们提供了一个 bootstrap 来减少单测中的重复代码:
|
445 |
|
446 | ```js
|
447 | const { app, mock, assert } = require('egg-mock/bootstrap');
|
448 |
|
449 | describe('test app', () => {
|
450 | it('should request success', () => {
|
451 | // mock data will be restored each case
|
452 | mock.data(app, 'method', { foo: 'bar' });
|
453 | return app.httpRequest()
|
454 | .get('/foo')
|
455 | .expect(res => {
|
456 | assert(!res.headers.foo);
|
457 | })
|
458 | .expect(/bar/);
|
459 | });
|
460 | });
|
461 |
|
462 | describe('test ctx', () => {
|
463 | it('can use ctx', async function() {
|
464 | const res = await this.ctx.service.foo();
|
465 | assert(res === 'foo');
|
466 | });
|
467 | });
|
468 | ```
|
469 |
|
470 | 我们将会在每个 case 中自定注入 ctx, 可以通过 `app.currentContext` 来获取当前的 ctx。
|
471 | 并且第一次使用 `app.mockContext` 会自动复用当前 case 的上下文。
|
472 |
|
473 | ```js
|
474 | const { app, mock, assert } = require('egg-mock/bootstrap');
|
475 |
|
476 | describe('test ctx', () => {
|
477 | it('should can use ctx', () => {
|
478 | const ctx = app.currentContext;
|
479 | assert(ctx);
|
480 | });
|
481 |
|
482 | it('should reuse ctx', () => {
|
483 | const ctx = app.currentContext;
|
484 | // 第一次调用会复用上下文
|
485 | const mockCtx = app.mockContext();
|
486 | assert(ctx === mockCtx);
|
487 | // 后续调用会新建上下文
|
488 | // 极不建议多次调用 app.mockContext
|
489 | // 这会导致上下文污染
|
490 | // 建议使用 app.mockContextScope
|
491 | const mockCtx2 = app.mockContext();
|
492 | assert(ctx !== mockCtx);
|
493 | });
|
494 | });
|
495 | ```
|
496 |
|
497 | ### env for custom bootstrap
|
498 | EGG_BASE_DIR: the base dir of egg app
|
499 | EGG_FRAMEWORK: the framework of egg app
|
500 |
|
501 | ## Questions & Suggestions
|
502 |
|
503 | Please open an issue [here](https://github.com/eggjs/egg/issues).
|
504 |
|
505 | ## License
|
506 |
|
507 | [MIT](LICENSE)
|
508 |
|
509 |
|
510 |
|
511 | ## Contributors
|
512 |
|
513 | |[<img src="https://avatars.githubusercontent.com/u/360661?v=4" width="100px;"/><br/><sub><b>popomore</b></sub>](https://github.com/popomore)<br/>|[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/227713?v=4" width="100px;"/><br/><sub><b>atian25</b></sub>](https://github.com/atian25)<br/>|[<img src="https://avatars.githubusercontent.com/u/985607?v=4" width="100px;"/><br/><sub><b>dead-horse</b></sub>](https://github.com/dead-horse)<br/>|[<img src="https://avatars.githubusercontent.com/u/452899?v=4" width="100px;"/><br/><sub><b>shepherdwind</b></sub>](https://github.com/shepherdwind)<br/>|[<img src="https://avatars.githubusercontent.com/u/456108?v=4" width="100px;"/><br/><sub><b>shaoshuai0102</b></sub>](https://github.com/shaoshuai0102)<br/>|
|
514 | | :---: | :---: | :---: | :---: | :---: | :---: |
|
515 | |[<img src="https://avatars.githubusercontent.com/u/2160731?v=4" width="100px;"/><br/><sub><b>mansonchor</b></sub>](https://github.com/mansonchor)<br/>|[<img src="https://avatars.githubusercontent.com/u/5856440?v=4" width="100px;"/><br/><sub><b>whxaxes</b></sub>](https://github.com/whxaxes)<br/>|[<img src="https://avatars.githubusercontent.com/u/3139237?v=4" width="100px;"/><br/><sub><b>brickyang</b></sub>](https://github.com/brickyang)<br/>|[<img src="https://avatars.githubusercontent.com/u/880513?v=4" width="100px;"/><br/><sub><b>zbinlin</b></sub>](https://github.com/zbinlin)<br/>|[<img src="https://avatars.githubusercontent.com/u/36814673?v=4" width="100px;"/><br/><sub><b>GoodMeowing</b></sub>](https://github.com/GoodMeowing)<br/>|[<img src="https://avatars.githubusercontent.com/u/5243774?v=4" width="100px;"/><br/><sub><b>ngot</b></sub>](https://github.com/ngot)<br/>|
|
516 | |[<img src="https://avatars.githubusercontent.com/u/3274850?v=4" width="100px;"/><br/><sub><b>geekdada</b></sub>](https://github.com/geekdada)<br/>|[<img src="https://avatars.githubusercontent.com/u/7784713?v=4" width="100px;"/><br/><sub><b>shinux</b></sub>](https://github.com/shinux)<br/>|[<img src="https://avatars.githubusercontent.com/u/7530656?v=4" width="100px;"/><br/><sub><b>zhang740</b></sub>](https://github.com/zhang740)<br/>|[<img src="https://avatars.githubusercontent.com/u/225856?v=4" width="100px;"/><br/><sub><b>caoer</b></sub>](https://github.com/caoer)<br/>|[<img src="https://avatars.githubusercontent.com/u/7970645?v=4" width="100px;"/><br/><sub><b>lidianhao123</b></sub>](https://github.com/lidianhao123)<br/>|[<img src="https://avatars.githubusercontent.com/u/9961514?v=4" width="100px;"/><br/><sub><b>limerickgds</b></sub>](https://github.com/limerickgds)<br/>|
|
517 | [<img src="https://avatars.githubusercontent.com/u/7971415?v=4" width="100px;"/><br/><sub><b>paranoidjk</b></sub>](https://github.com/paranoidjk)<br/>|[<img src="https://avatars.githubusercontent.com/u/1207064?v=4" width="100px;"/><br/><sub><b>gxcsoccer</b></sub>](https://github.com/gxcsoccer)<br/>|[<img src="https://avatars.githubusercontent.com/u/2127199?v=4" width="100px;"/><br/><sub><b>okoala</b></sub>](https://github.com/okoala)<br/>|[<img src="https://avatars.githubusercontent.com/u/10825163?v=4" width="100px;"/><br/><sub><b>ImHype</b></sub>](https://github.com/ImHype)<br/>|[<img src="https://avatars.githubusercontent.com/u/16033313?v=4" width="100px;"/><br/><sub><b>linjiajian999</b></sub>](https://github.com/linjiajian999)<br/>
|
518 |
|
519 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Fri Apr 29 2022 22:49:14 GMT+0800`.
|
520 |
|
521 |
|