Aller au contenu principal
Version: 3.0.0

Test

In application development, testing is very important. In the period of rapid iteration of traditional Web products, each test case provides a guarantee for the stability of the application. API upgrade, test cases can well check whether the code is backwards compatible. For all possible inputs, once the test covers, its output can be clarified. After the code changes, you can judge whether the code changes affect the determined results through the test results.

Therefore, the Controller, Service and other codes of the application must have corresponding unit tests to ensure the code quality. Of course, each functional change and refactoring of the framework and components requires corresponding unit tests, and the modified code is required to be covered by the 100% as much as possible.

The current testing libraries in the community are mainly jest and mocha. This article uses jest as an example.

Test directory structure

We agree that the test directory is the directory where all test scripts are stored, and the fixtures used for testing and related auxiliary scripts should be placed in this directory.

The test script file is named ${filename}.test.ts and must be suffixed with .test.ts.

An example of an application's test directory:

➜  my_midway_app tree
.
├── src
├── test
│ └── controller
│ └── home.controller.test.ts
├── package.json
└── tsconfig.json

Test Run Tool

By default, Midway provides the midway-bin command to run the test script. In the new version, Midway replaces mocha with Jest by default. It is more powerful and more integrated, which allows us to focus on writing test code instead of choosing those test peripheral tools and modules.

You only need to configure the scripts.test on the package.json.

{
"scripts": {
"test": "jest"
}
}

Then you can run the test according to the standard npm test. In the default scaffolding, we have provided this command, so you can run the test out of the box.

➜ my_midway_app npm run test

> my_midway_project@1.0.0 test /Users/harry/project/application/my_midway_app
>jest

Testing all *.test.ts...
PASS test/controller/home.controller.test.ts
PASS test/controller/api.controller.test.ts

Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.26 seconds
Ran all test suites matching /\/test\/[^.]*\.test\.ts$/i.

Assertion library

Jest has a powerful expect assertion library, which can be directly used in the global.

For example, commonly used.

Expect (result.status).toBe(200); // Whether the value is equal to a certain value, the reference is equal
expect(result.status).not.toBe(200);
Expect (result).toEqual('hello'); //Simple match, the same object attribute is also true
Expect (result).toStrictEqual('hello'); // Strictly match
Expect (['lime', 'apple']).toContain('lime'); //Judge whether it is in an array

For more information about assertion methods, see https:// jestjs.io/docs/en/expect.

Create test

Different upper-level frameworks have different testing methods. Take the most commonly used HTTP service as an example. If you need to test an HTTP service, generally speaking, we need to create an HTTP service and then request it with the client.

Midway provides a basic set of @midwayjs/mock tools to help the upper framework test in this area. At the same time, it also provides convenient methods to create Framework,App and close.

The whole process approach is divided into several parts:

  • createApp to create an app object for a Framework
  • close closes a Framework or an app

In order to keep the test simple, the whole process currently reveals these two methods.

// create app
const app = await createApp<Framework>();

The Framework passed in here is used to derive the type for the TypeScript. In this way, the main frame app instance can be returned.

After the app is run, you can use the close method to close the app.

import { createApp, close } from '@midwayjs/mock';

await close(app);

In fact, @midwayjs/bootstrap is encapsulated in createApp method, and interested partners can read the source code.

Test HTTP service

In addition to creating app, @midwayjs/mock also provides a simple client method for quickly creating test behaviors corresponding to various services.

For example, for HTTP, we encapsulate supertest and provide createHttpRequest methods to create HTTP clients.

// Create a client request
const result = await createHttpRequest(app).get('/');
// Test returns results
expect(result.text).toBe('Hello Midwayjs!');

It is recommended to reuse the app instance in a test file. The complete test example is as follows.

import { createApp, close, createHttpRequest } from '@midwayjs/mock';
import { Framework, Application } from '@midwayjs/koa';
import * as assert from 'assert';

describe('test/controller/home.test.ts', () => {

let app: Application;

beforeAll(async () => {
// Create app only once and can be reused.
app = await createApp<Framework>();
});

afterAll(async () => {
// close app
await close(app);
});

it('should GET /', async () => {
// make request
const result = await createHttpRequest(app)
.get('/')
.set('x-timeout', '5000');

// use expect by jest
expect(result.status).toBe(200);
expect(result.text).toBe('Hello Midwayjs!');

// or use assert
assert.deepStrictEqual(result.status, 200);
assert.deepStrictEqual(result.text, 'Hello Midwayjs!');
});

it('should POST /', async () => {
// make request
const result = await createHttpRequest(app)
.post('/')
.send({id: '1'});

// use expect by jest
expect(result.status).toBe(200);
});

});

Example:

Create a get request and pass the query parameter.

const result = await createHttpRequest(app)
.get('/set_header')
.query({ name: 'harry' });

create a post request and pass the body parameter.

const result = await createHttpRequest(app)
.post('/user/catchThrowWithValidate')
.send({id: '1'});

create a post request and pass the form body parameter.

const result = await createHttpRequest(app)
.post('/param/body')
.type('form')
.send({id: '1'})

Pass the header header.

const result = await createHttpRequest(app)
.get('/set_header')
.set({
'x-bbb ': ' 123'
})
.query({ name: 'harry' });

Pass cookie.

const cookie = [
"koa.sess=eyJuYW1lIjoiaGFycnkiLCJfZXhwaXJlIjoxNjE0MTQ5OTQ5NDcyLCJfbWF4QWdlIjo4NjQwMDAwMH0=; path=/; expires=Wed, 24 Feb 2021 06:59:09 GMT; httponly ",
"koa.sess.sig=mMRQWascH-If2-BC7v8xfRbmiNo; path=/; expires=Wed, 24 Feb 2021 06:59:09 GMT; httponly"
]

const result = await createHttpRequest(app)
.get('/set_header')
.set('Cookie', cookie)
.query({ name: 'harry' });

Test service

Outside the controller, sometimes we need to test a single service, which we can get from the dependency injection container.

Assume that a test UserService is required.

// src/service/user.ts
import { Provide } from '@midwayjs/core';

@Provide()
export class UserService {
async getUser() {
// xxx
}
}

Then write this in the test code.

import { createApp, close, createHttpRequest } from '@midwayjs/mock';
import { Framework } from '@midwayjs/web';
import * as assert from 'assert';
import { UserService } from '../../src/service/user';

describe('test/controller/home.test.ts', () => {

it('should GET /', async () => {
// create app
const app = await createApp<Framework>();

// Obtain instances based on dependency injection class (recommended)
const userService = await app.getApplicationContext().getAsync<UserService>(UserService);
// Get the instance based on the dependency injection Id.
const userService = await app.getApplicationContext().getAsync<UserService>('userService');
// Incoming class Ignoring Generics can also correctly derive
const userService = await app.getApplicationContext().getAsync(UserService);

// close app
await close(app);
});

});

If your service is associated with a request (ctx), you can use the request scope to get the service.

import { createApp, close, createHttpRequest } from '@midwayjs/mock';
import { Framework } from '@midwayjs/web';
import * as assert from 'assert';
import { UserService } from '../../src/service/user';

describe('test/controller/home.test.ts', () => {

it('should GET /', async () => {
// create app
const app = await createApp<Framework>();

// Get the instance based on the dependency injection Id.
const userService = await app.createAnonymousContext()
.requestContext.getAsync<UserService>('userService');

// You can also pass in class to get an instance.
const userService = await app.createAnonymousContext()
.requestContext.getAsync(UserService);

// close app
await close(app);
});

});

createApp option parameters

createApp method is used to create an app instance of a framework, and by passing in a generic framework type, the app we infer can be the app returned by the framework.

For example:

import { Framework } from '@midwayjs/grpc';

// The app here can ensure that it is the app returned by the gRPC framework.
const app = await createApp<Framework>();

createApp method actually has parameters, and its method signature is as follows.

async createApp (
appDir = process.cwd()
options: IConfigurationOptions = {}
)

The first parameter is the absolute root directory path of the project, which defaults to process.cwd(). The second parameter is the Bootstrap startup parameter, such as the configuration of some global behaviors. for details, see TS definition.

Close option parameter

The close method is used to close the framework related to the app instance.

await close(app);

It has some parameters.

export declare function close (
app: IMidwayApplication | IMidwayFramework<any, any>
options ?: {
cleanLogsDir?: boolean;
cleanTempDir?: boolean;
sleep?: number;
}): Promise<void>;

The first parameter is an instance of app or framework.

The second parameter is an object that can perform some behaviors when executing shutdown:

    1. cleanLogsDir defaults to false, and deletes the logs directory after the control test is completed (except windows)
    1. The default cleanTempDir is false, and some temporary directories (such as run directory generated by egg) are cleaned up.
    1. The default value of sleep is 50, in milliseconds, and the delay time after the app is turned off (to prevent the logs from being written without success).

Test with bootstrap files

In general, you don't need to use bootstrap.js to test. If you want to test directly using the bootstrap.js entry file, you can pass the entry file information during the test.

Unlike dev/test startup, startup using bootstrap.js is a real service that runs multiple frameworks at the same time and creates app instances of multiple frameworks.

@midwayjs/mock provides createBootstrap methods to test the startup file type. We can pass in the bootstrap.js of the entry file as a startup parameter, so that createBootstrap method starts the code through the entry file.

it('should GET /', async () => {
// create app
const bootstrap = await createBootstrap(join(process.cwd(), 'bootstrap.js'));
// Get an app instance based on the frame type.
const app = bootstrap.getApp('koa');

// expect and test

// close bootstrap
await bootstrap.close();
});

Run a single test

Unlike the only of mocha, the only method of jest takes effect only for a single file. midway-bin provides the ability to run a single file.

Execute a single file.

$ jest test/controller/api.ts

If you want to run a specific test in a file, you can use jest's -t or --testNamePattern option, followed by the name of the test you want to run. For example:

$ jest -t "name of your test"

This will only run tests with matching names.

Customize Jest file content

In general, the Midway tool chain has built-in jest configuration, so that users do not need to add this file. However, in some special scenarios, such as using VSCode or Idea editors, you may need to specify a jest.config.js scenario when you need to develop and test in the visualization area. In this case, Midway supports creating a custom jest configuration file.

Create a jest.config.js file in the root directory of the project.

➜  my_midway_app tree
.
├── src
├── test
│ └── controller
│ └── home.test.ts
├── jest.config.js
├── package.json
└── tsconfig.json

The content is as follows. The configuration is the same as the standard jest.

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures']
coveragePathIgnorePatterns: ['<rootDir>/test/']
};

Common settings

If you need to run some code before a single test, you can add jest.setup.js.

const path = require('path');

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures']
coveragePathIgnorePatterns: ['<rootDir>/test/']
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], // read jest.setup.js in advance
};
attention

Note that jest.setup.js can only use js files.

Example 1: The problem of long test code time

If the following error occurs in the test, it means that your code takes a long time to execute (such as connecting to the database, running tasks, etc.). If you are sure that there is no problem with the code, you need to extend the startup time.

Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.

The default time for jest is 5000ms(5 seconds). You can adjust it to more.

Can be modified at startup via package.json.

{
"scripts": {
"test": "jest --testTimeout=30000"
}
}

You can write the following code in the jest.setup.js file to adjust the jest timeout period.

// jest.setup.js
jest.setTimeout(30000);

Example 2: Global Environment Variables

Similarly, jest.setup.js can also run custom code, such as setting global environment variables.

// jest.setup.js
process.env.MIDWAY_TS_MODE = 'true';

Example 3: Processing where the program cannot exit normally

Sometimes, because some codes (timers, listeners, etc.) run in the background, the process cannot be exited after a single test run. For this case, jest provides the -forceExit parameter.

$ jest --forceExit
$ jest --coverage --forceExit

You can also add attributes to a custom file.

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures']
coveragePathIgnorePatterns: ['<rootDir>/test/']
forceExit: true
};

Example 4: Parallel Change to Serial Execution

By default, jest processes each test file in parallel. If there are scenarios such as startup ports in the test code, parallel processing may cause port conflicts and report errors. At this time, you need to add the -runInBand parameter. Note that this parameter can only be loaded in the command.

$ jest --runInBand
$ jest --coverage --runInBand

Editor configuration

Jetbrain Webstorm/Idea configuration

In the Jetbrain editor, the "jest" plug-in needs to be enabled. Since the sub-process is used to start, we still need to specify the load -require = ts-node/register at startup.

image.png

VSCode configuration

Search the plug-in first and install Jest Runner. image.png Open the configuration and configure the jest command path.

image.png

Enter node -- require = ts-node/register ./node_modules/.bin/jest at the jest command.

Or set settings.json in the workspace folder. vscode.

{
"jest.pathToJest": "node --require=ts-node/register ./node_modules/.bin/jest --detectOpenHandles ",
"jestrunner.jestCommand": "node --require=ts-node/register ./node_modules/.bin/jest --detectOpenHandles"
}

Since the debugging of the jest runner plug-in uses the debugging of VSNode, the launch.json of VSNode needs to be configured separately.

Set launch.json in the folder. vscode

{
"version": "0.0.1 ",
"configurations": [
{
"name": "Debug Jest Tests ",
"type": "node ",
"request": "launch ",
"runtimeArgs": [
"--inspect-brk ",
"--require=ts-node/register ",
"${workspaceRoot}/node_modules/.bin/jest ",
"--runInBand ",
"--detectOpenHandles"
],
"console": "integratedTerminal ",
"internalConsoleOptions": "neverOpen"
}
]
}

About alias paths

The mwtsc tool does not support the Alias Path feature.

About mock data

Simulation data is a capability that can be used in both development and testing. For more information, see Simulation data.