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 | Mock library for testing Egg applications, plugins and custom Egg frameworks with ease. `egg-mock` inherits all APIs from [node_modules/mm](https://github.com/node-modules/mm), offering more flexibility.
|
16 |
|
17 | ## Install
|
18 |
|
19 | ```bash
|
20 | $ npm i egg-mock --save-dev
|
21 | ```
|
22 |
|
23 | ## Usage
|
24 |
|
25 | ### Create testcase
|
26 |
|
27 | Launch a mock server with `mm.app`
|
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 | });
|
40 | return app.ready();
|
41 | })
|
42 | after(() => app.close());
|
43 |
|
44 | it('should request /', () => {
|
45 | return app.httpRequest()
|
46 | .get('/')
|
47 | .expect(200);
|
48 | });
|
49 | });
|
50 | ```
|
51 |
|
52 | Retrieve Agent instance through `app.agent` after `mm.app` started.
|
53 |
|
54 | Using `mm.cluster` launch cluster server, you can use the same API as `mm.app`;
|
55 |
|
56 | ### Test Application
|
57 |
|
58 | `baseDir` is optional that is `process.cwd()` by default.
|
59 |
|
60 | ```js
|
61 | before(() => {
|
62 | app = mm.app();
|
63 | return app.ready();
|
64 | });
|
65 | ```
|
66 |
|
67 | ### Test Framework
|
68 |
|
69 | framework is optional, it's `node_modules/egg` by default.
|
70 |
|
71 | ```js
|
72 | before(() => {
|
73 | app = mm.app({
|
74 | baseDir: 'apps/demo',
|
75 | framework: true,
|
76 | });
|
77 | return app.ready();
|
78 | });
|
79 | ```
|
80 |
|
81 | ### Test Plugin
|
82 |
|
83 | If `eggPlugin.name` is defined in `package.json`, it's a plugin that will be loaded to plugin list automatically.
|
84 |
|
85 | ```js
|
86 | before(() => {
|
87 | app = mm.app({
|
88 | baseDir: 'apps/demo',
|
89 | });
|
90 | return app.ready();
|
91 | });
|
92 | ```
|
93 |
|
94 | You can also test the plugin in different framework, e.g. test [aliyun-egg](https://github.com/eggjs/aliyun-egg) and framework-b in one plugin.
|
95 |
|
96 | ```js
|
97 | describe('aliyun-egg', () => {
|
98 | let app;
|
99 | before(() => {
|
100 | app = mm.app({
|
101 | baseDir: 'apps/demo',
|
102 | framework: path.join(__dirname, 'node_modules/aliyun-egg'),
|
103 | });
|
104 | return app.ready();
|
105 | });
|
106 | });
|
107 |
|
108 | describe('framework-b', () => {
|
109 | let app;
|
110 | before(() => {
|
111 | app = mm.app({
|
112 | baseDir: 'apps/demo',
|
113 | framework: path.join(__dirname, 'node_modules/framework-b'),
|
114 | });
|
115 | return app.ready();
|
116 | });
|
117 | });
|
118 | ```
|
119 |
|
120 | If it's detected as an plugin, but you don't want it to be, you can use `plugin = false`.
|
121 |
|
122 | ```js
|
123 | before(() => {
|
124 | app = mm.app({
|
125 | baseDir: 'apps/demo',
|
126 | plugin: false,
|
127 | });
|
128 | return app.ready();
|
129 | });
|
130 | ```
|
131 |
|
132 | ## API
|
133 |
|
134 | ### mm.app(options)
|
135 |
|
136 | Create a mock application.
|
137 |
|
138 | ### mm.cluster(options)
|
139 |
|
140 | Create a mock cluster server, but you can't use API in application, you should test using `supertest`.
|
141 |
|
142 | ```js
|
143 | const mm = require('egg-mock');
|
144 | describe('test/app.js', () => {
|
145 | let app, config;
|
146 | before(() => {
|
147 | app = mm.cluster();
|
148 | return app.ready();
|
149 | });
|
150 | after(() => app.close());
|
151 |
|
152 | it('some test', () => {
|
153 | return app.httpRequest()
|
154 | .get('/config')
|
155 | .expect(200)
|
156 | });
|
157 | });
|
158 | ```
|
159 |
|
160 | You can disable coverage, because it's slow.
|
161 |
|
162 | ```js
|
163 | mm.cluster({
|
164 | coverage: false,
|
165 | });
|
166 | ```
|
167 |
|
168 | ### mm.env(env)
|
169 |
|
170 | Mock env when starting
|
171 |
|
172 | ```js
|
173 | // production environment
|
174 | mm.env('prod');
|
175 | mm.app({
|
176 | cache: false,
|
177 | });
|
178 | ```
|
179 |
|
180 | Environment list <https://github.com/eggjs/egg-core/blob/master/lib/loader/egg_loader.js#L82>
|
181 |
|
182 | ### mm.consoleLevel(level)
|
183 |
|
184 | Mock level that print to stdout/stderr
|
185 |
|
186 | ```js
|
187 | // DON'T log to terminal
|
188 | mm.consoleLevel('NONE');
|
189 | ```
|
190 |
|
191 | level list: `DEBUG`, `INFO`, `WARN`, `ERROR`, `NONE`
|
192 |
|
193 | ### mm.home(homePath)
|
194 |
|
195 | mock home directory
|
196 |
|
197 | ### mm.restore()
|
198 |
|
199 | restore all mock data, e.g. `afterEach(mm.restore)`
|
200 |
|
201 | ### options
|
202 |
|
203 | Options for `mm.app` and `mm.cluster`
|
204 |
|
205 | #### baseDir {String}
|
206 |
|
207 | The directory of application, default is `process.cwd()`.
|
208 |
|
209 | ```js
|
210 | mm.app({
|
211 | baseDir: path.join(__dirname, 'fixtures/apps/demo'),
|
212 | })
|
213 | ```
|
214 |
|
215 | You can use a string based on `$CWD/test/fixtures` for short
|
216 |
|
217 | ```js
|
218 | mm.app({
|
219 | baseDir: 'apps/demo',
|
220 | })
|
221 | ```
|
222 |
|
223 | #### framework {String/Boolean}
|
224 |
|
225 | The directory of framework
|
226 |
|
227 | ```js
|
228 | mm.app({
|
229 | baseDir: 'apps/demo',
|
230 | framework: path.join(__dirname, 'fixtures/egg'),
|
231 | })
|
232 | ```
|
233 |
|
234 | It can be true when test an framework
|
235 |
|
236 | #### plugin
|
237 |
|
238 | The directory of plugin, it's detected automatically.
|
239 |
|
240 | ```js
|
241 | mm.app({
|
242 | baseDir: 'apps/demo',
|
243 | })
|
244 | ```
|
245 |
|
246 | #### plugins {Object}
|
247 |
|
248 | Define a list of plugins
|
249 |
|
250 | #### cache {Boolean}
|
251 |
|
252 | Determine whether enable cache. it's cached by baseDir.
|
253 |
|
254 | #### clean {Boolean}
|
255 |
|
256 | Clean all logs directory, default is true.
|
257 |
|
258 | If you are using `ava`, disable it.
|
259 |
|
260 | ### app.mockLog([logger]) and app.expectLog(str[, logger]), app.notExpectLog(str[, logger])
|
261 |
|
262 | Assert some string value in the logger instance.
|
263 | It is recommended to pair `app.mockLog()` with `app.expectLog()` or `app.notExpectLog()`.
|
264 | Using `app.expectLog()` or `app.notExpectLog()` alone requires dependency on the write speed of the log. When the server disk is high IO, unstable results will occur.
|
265 |
|
266 | ```js
|
267 | it('should work', async () => {
|
268 | app.mockLog();
|
269 | await app.httpRequest()
|
270 | .get('/')
|
271 | .expect('hello world')
|
272 | .expect(200);
|
273 |
|
274 | app.expectLog('foo in logger');
|
275 | app.expectLog('foo in coreLogger', 'coreLogger');
|
276 | app.expectLog('foo in myCustomLogger', 'myCustomLogger');
|
277 |
|
278 | app.notExpectLog('bar in logger');
|
279 | app.notExpectLog('bar in coreLogger', 'coreLogger');
|
280 | app.notExpectLog('bar in myCustomLogger', 'myCustomLogger');
|
281 | });
|
282 | ```
|
283 |
|
284 | ### app.httpRequest()
|
285 |
|
286 | Request current app http server.
|
287 |
|
288 | ```js
|
289 | it('should work', () => {
|
290 | return app.httpRequest()
|
291 | .get('/')
|
292 | .expect('hello world')
|
293 | .expect(200);
|
294 | });
|
295 | ```
|
296 |
|
297 | See [supertest](https://github.com/visionmedia/supertest) to get more APIs.
|
298 |
|
299 | #### .unexpectHeader(name)
|
300 |
|
301 | Assert current response not contains the specified header
|
302 |
|
303 | ```js
|
304 | it('should work', () => {
|
305 | return app.httpRequest()
|
306 | .get('/')
|
307 | .unexpectHeader('set-cookie')
|
308 | .expect(200);
|
309 | });
|
310 | ```
|
311 |
|
312 | #### .expectHeader(name)
|
313 |
|
314 | Assert current response contains the specified header
|
315 |
|
316 | ```js
|
317 | it('should work', () => {
|
318 | return app.httpRequest()
|
319 | .get('/')
|
320 | .expectHeader('set-cookie')
|
321 | .expect(200);
|
322 | });
|
323 | ```
|
324 |
|
325 | ### app.mockContext(options)
|
326 |
|
327 | ```js
|
328 | const ctx = app.mockContext({
|
329 | user: {
|
330 | name: 'Jason'
|
331 | }
|
332 | });
|
333 | console.log(ctx.user.name); // Jason
|
334 | ```
|
335 |
|
336 | ### app.mockContextScope(fn, options)
|
337 |
|
338 | ```js
|
339 | await app.mockContextScope(async ctx => {
|
340 | console.log(ctx.user.name); // Jason
|
341 | }, {
|
342 | user: {
|
343 | name: 'Jason'
|
344 | }
|
345 | });
|
346 | ```
|
347 |
|
348 | ### app.mockCookies(data)
|
349 |
|
350 | ```js
|
351 | app.mockCookies({
|
352 | foo: 'bar'
|
353 | });
|
354 | const ctx = app.mockContext();
|
355 | console.log(ctx.getCookie('foo'));
|
356 | ```
|
357 |
|
358 | ### app.mockHeaders(data)
|
359 |
|
360 | Mock request header
|
361 |
|
362 | ### app.mockSession(data)
|
363 |
|
364 | ```js
|
365 | app.mockSession({
|
366 | foo: 'bar'
|
367 | });
|
368 | const ctx = app.mockContext();
|
369 | console.log(ctx.session.foo);
|
370 | ```
|
371 |
|
372 | ### app.mockService(service, methodName, fn)
|
373 |
|
374 | ```js
|
375 | it('should mock user name', function* () {
|
376 | app.mockService('user', 'getName', function* (ctx, methodName, args) {
|
377 | return 'popomore';
|
378 | });
|
379 | const ctx = app.mockContext();
|
380 | yield ctx.service.user.getName();
|
381 | });
|
382 | ```
|
383 |
|
384 | ### app.mockServiceError(service, methodName, error)
|
385 |
|
386 | You can mock an error for service
|
387 |
|
388 | ```js
|
389 | app.mockServiceError('user', 'home', new Error('mock error'));
|
390 | ```
|
391 |
|
392 | ### app.mockCsrf()
|
393 |
|
394 | ```js
|
395 | app.mockCsrf();
|
396 |
|
397 | return app.httpRequest()
|
398 | .post('/login')
|
399 | .expect(302);
|
400 | ```
|
401 |
|
402 | ### app.mockHttpclient(url, method, data)
|
403 |
|
404 | Mock httpclient request, e.g.: `ctx.curl`
|
405 |
|
406 | ```js
|
407 | app.get('/', function*() {
|
408 | const ret = yield this.curl('https://eggjs.org');
|
409 | this.body = ret.data.toString();
|
410 | });
|
411 |
|
412 | app.mockHttpclient('https://eggjs.org', {
|
413 | // can be buffer / string / json / function
|
414 | // will auto convert to buffer
|
415 | // follow options.dataType to convert
|
416 | data: 'mock egg',
|
417 | });
|
418 | // app.mockHttpclient('https://eggjs.org', 'get', mockResponse); // mock get
|
419 | // app.mockHttpclient('https://eggjs.org', [ 'get' , 'head' ], mockResponse); // mock get and head
|
420 | // app.mockHttpclient('https://eggjs.org', '*', mockResponse); // mock all methods
|
421 | // app.mockHttpclient('https://eggjs.org', mockResponse); // mock all methods by default
|
422 | // app.mockHttpclient('https://eggjs.org', 'get', function(url, opt) { return 'xxx' }); // support fn
|
423 |
|
424 | return app.httpRequest()
|
425 | .post('/')
|
426 | .expect('mock egg');
|
427 | ```
|
428 |
|
429 | You can also use Regular Expression for matching url.
|
430 |
|
431 | ```js
|
432 | app.mockHttpclient(/\/users\/[a-z]$/i, {
|
433 | data: {
|
434 | name: 'egg',
|
435 | },
|
436 | });
|
437 | ```
|
438 |
|
439 | You can alse mock agent.httpclient
|
440 |
|
441 | ```js
|
442 | app.agent.mockHttpclient('https://eggjs.org', {
|
443 | data: {
|
444 | name: 'egg',
|
445 | },
|
446 | });
|
447 | ```
|
448 |
|
449 | ## Bootstrap
|
450 |
|
451 | We also provide a bootstrap file for applications' unit test to reduce duplicated code:
|
452 |
|
453 | ```js
|
454 | const { app, mock, assert } = require('egg-mock/bootstrap');
|
455 |
|
456 | describe('test app', () => {
|
457 | it('should request success', () => {
|
458 | // mock data will be restored each case
|
459 | mock.data(app, 'method', { foo: 'bar' });
|
460 | return app.httpRequest()
|
461 | .get('/foo')
|
462 | .expect(res => {
|
463 | assert(!res.headers.foo);
|
464 | })
|
465 | .expect(/bar/);
|
466 | });
|
467 | });
|
468 |
|
469 | describe('test ctx', () => {
|
470 | it('can use ctx', async function() {
|
471 | const res = await this.ctx.service.foo();
|
472 | assert(res === 'foo');
|
473 | });
|
474 | });
|
475 | ```
|
476 |
|
477 | We inject ctx to every test case, so you can use `app.currentContext` in your test case.
|
478 | and the first call of `app.mockContext` will reuse `app.currentContext`.
|
479 |
|
480 | ```js
|
481 | const { app, mock, assert } = require('egg-mock/bootstrap');
|
482 |
|
483 | describe('test ctx', () => {
|
484 | it('should can use ctx', () => {
|
485 | const ctx = app.currentContext;
|
486 | assert(ctx);
|
487 | });
|
488 |
|
489 | it('should reuse ctx', () => {
|
490 | const ctx = app.currentContext;
|
491 | // first call will reuse app.currentContext
|
492 | const mockCtx = app.mockContext();
|
493 | assert(ctx === mockCtx);
|
494 | // next call will create a new context
|
495 | // multi call app.mockContext will get wrong context with app.currentContext
|
496 | // so we recommend to use app.mockContextScope
|
497 | const mockCtx2 = app.mockContext();
|
498 | assert(ctx !== mockCtx);
|
499 | });
|
500 | });
|
501 | ```
|
502 |
|
503 | And if you use mm.app to bootstrap app, you can manually call setGetAppCallback,
|
504 | then egg-mock will inject ctx for each test case.
|
505 | ```js
|
506 | // test/.setup.js
|
507 | const mm = require('egg-mock');
|
508 | const path = require('path');
|
509 | before(async function() {
|
510 | const app = this.app = mm.app();
|
511 | mm.setGetAppCallback(() => {
|
512 | return app;
|
513 | });
|
514 | await app.ready();
|
515 | });
|
516 |
|
517 |
|
518 | // test/index.test.js
|
519 | it('should work', function() {
|
520 | // eslint-disable-next-line no-undef
|
521 | assert(this.app.currentContext);
|
522 | });
|
523 | ```
|
524 |
|
525 | ### env for custom bootstrap
|
526 | EGG_BASE_DIR: the base dir of egg app
|
527 | EGG_FRAMEWORK: the framework of egg app
|
528 |
|
529 | ## Questions & Suggestions
|
530 |
|
531 | Please open an issue [here](https://github.com/eggjs/egg/issues).
|
532 |
|
533 | ## License
|
534 |
|
535 | [MIT](LICENSE)
|
536 |
|
537 |
|
538 |
|
539 | ## Contributors
|
540 |
|
541 | |[<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/>|
|
542 | | :---: | :---: | :---: | :---: | :---: | :---: |
|
543 | |[<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/>|
|
544 | |[<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/>|
|
545 | [<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/>
|
546 |
|
547 | 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`.
|
548 |
|
549 |
|