UNPKG

14.6 kBMarkdownView Raw
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
31const path = require('path');
32const mm = require('egg-mock');
33
34describe('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
62before(() => {
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
75before(() => {
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
89before(() => {
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
101describe('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
112describe('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
127before(() => {
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
148const mm = require('egg-mock');
149describe('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
168mm.cluster({
169 coverage: false,
170});
171```
172
173### mm.env(env)
174
175设置环境变量,主要用于启动阶段,运行阶段可以使用 app.mockEnv。
176
177```js
178// 模拟生成环境
179mm.env('prod');
180mm.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
189mock 终端日志打印级别
190
191```js
192// 不输出到终端
193mm.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
208mm.app 和 mm.cluster 的配置参数
209
210#### baseDir {String}
211
212当前应用的目录,如果是应用本身的测试可以不填默认为 $CWD。
213
214指定完整路径
215
216```js
217mm.app({
218 baseDir: path.join(__dirname, 'fixtures/apps/demo'),
219})
220```
221
222也支持缩写,找 test/fixtures 目录下的
223
224```js
225mm.app({
226 baseDir: 'apps/demo',
227})
228```
229
230#### customEgg {String/Boolean}
231
232指定框架路径
233
234```js
235mm.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
248mm.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
277it('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
300it('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
315it('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
328it('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
341const ctx = app.mockContext({
342 user: {
343 name: 'Jason'
344 }
345});
346console.log(ctx.user.name); // Jason
347```
348
349
350### app.mockContextScope(fn, options)
351
352安全的模拟上下文数据,同一用例用多次调用 mockContext 可能会造成 AsyncLocalStorage 污染
353
354```js
355await 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
367app.mockCookies({
368 foo: 'bar'
369});
370const ctx = app.mockContext();
371console.log(ctx.getCookie('foo'));
372```
373
374### app.mockHeaders(data)
375
376模拟请求头
377
378### app.mockSession(data)
379
380```js
381app.mockSession({
382 foo: 'bar'
383});
384const ctx = app.mockContext();
385console.log(ctx.session.foo);
386```
387
388### app.mockService(service, methodName, fn)
389
390```js
391it('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
405app.mockServiceError('user', 'home', new Error('mock error'));
406```
407
408### app.mockCsrf()
409
410模拟 csrf,不用传递 token
411
412```js
413app.mockCsrf();
414
415return app.httpRequest()
416 .post('/login')
417 .expect(302);
418```
419
420### app.mockHttpclient(url, method, data)
421
422模拟 httpclient 的请求,例如 `ctx.curl`
423
424```js
425app.get('/', async ctx => {
426 const ret = await ctx.curl('https://eggjs.org');
427 this.body = ret.data.toString();
428});
429
430app.mockHttpclient('https://eggjs.org', {
431 // 模拟的参数,可以是 buffer / string / json / function
432 // 都会转换成 buffer
433 // 按照请求时的 options.dataType 来做对应的转换
434 data: 'mock egg',
435});
436
437return app.httpRequest()
438 .post('/')
439 .expect('mock egg');
440```
441
442## Bootstrap
443
444我们提供了一个 bootstrap 来减少单测中的重复代码:
445
446```js
447const { app, mock, assert } = require('egg-mock/bootstrap');
448
449describe('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
462describe('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
474const { app, mock, assert } = require('egg-mock/bootstrap');
475
476describe('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
498EGG_BASE_DIR: the base dir of egg app
499EGG_FRAMEWORK: the framework of egg app
500
501## Questions & Suggestions
502
503Please open an issue [here](https://github.com/eggjs/egg/issues).
504
505## License
506
507[MIT](LICENSE)
508
509<!-- GITCONTRIBUTOR_START -->
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
519This 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<!-- GITCONTRIBUTOR_END -->