This article first appeared in my blog, welcome to check out ~

preface

In January of this year I wrote a Vue+Koa full stack application, and the corresponding tutorial, and got a lot of good reviews. At the same time, IN the process of communicating with readers, I have been constantly aware of the shortcomings and shortcomings, so I have also been updated and improved. The enhancements brought this time are the addition and complete front and back end testing. I believe that for a lot of learning front end friends, testing this thing seems to be a familiar stranger. You’ve heard it, but you haven’t done it. If you’re familiar with front-end (and NodeJS side) testing, this article might not be very helpful, but I’d love to get your input!

Introduction to the

Similar to the previous article full stack development practice: Vue2+Koa1 development of a complete front and back end project, this article from the test novice’s point of view (default understanding of Koa and put into practice, understand Vue and put into practice, but no test experience), on the existing project from zero to build our full stack test system. You can learn about the meaning of testing, the construction of Jest testing framework, the similarities and differences of front and back end testing, how to write test cases, how to view test results and improve our test coverage, whether 100% test coverage is necessary, and various difficulties encountered in the construction of test environment and testing itself. Hopefully this will serve as an introduction to front-end and Node side testing.

The project structure

With the previous project structure as a skeleton, it’s easy to add the Jest testing framework.

.├ ── LICENSE ├─ readme.md ├─.env // Environment Variables Config File ├─ app.js // Koa import File ├─ build // vue │ ├─ Build.js │ ├─ Check-versions. Js │ ├─ dev-client.js │ ├── Utils.js │ ├── Webpack. Base. Conf. Js │ ├ ─ ─ webpack. Dev. Conf., js │ └ ─ ─ webpack. Prod. Conf., js ├ ─ ─ the config / / vue - cli generates & yourself with some of the configuration file │ ├ ─ ─ . The default. The conf │ ├ ─ ─ dev env. Js │ ├ ─ ─ index. The js │ └ ─ ─ the prod. Env. Js ├ ─ ─ dist / / Vue folder after the build │ ├ ─ ─ index. The HTML file │ / / entry └ ─ ─ the static / / static resources ├ ─ ─ env. Js/switch/environment variables related < - new ├ ─ ─ the env / / development, online environment variable < - new ├ ─ ─ the env. The test environment variable < / / test - new ├ ─ ─ Index.html // VUE - The main HTML file generated by the CLI to hold vUE components. HTML ├─ Package. json // NPM dependency, Project info file, Jest configuration item <-- New ├─ server // Koa │ ├─ Api │ ├─ Controllers │ ├─ Models │ ├─ routes // Routes │ ├─ Api │ ├─ Config │ ├─ Controllers │ ├─ Models │ ├─ routes │ ├─ Routes │ ├─ Controllers │ ├─ Models │ ├─ routes │ ├─ Routes │ ├─ Controllers │ ├─ Models │ ├─ Routes │ └ ─ ─ schema / / schema - database table structure ├ ─ ─ SRC / / vue - cli generate utils tools & oneself add │ ├ ─ ─ App. Vue / / master file │ ├ ─ ─ assets / / related static resources stored │ ├ ─ ─ Components / / single file component │ ├ ─ ─ the main, js / / introduce Vue resources, such as mount Vue entry js │ └ ─ ─ utils/tools/folder - encapsulate reusable method, function ├ ─ ─test│ ├ ─ ─ sever / / server test < - new │ └ ─ ─ client / / client (front) test "- a new └ ─ ─ yarn. The lock / / yarn generated automatically lock fileCopy the code

You can see that there are only a few new additions or updates:

  1. The primary test folder contains the client (front-end) and server test files
  2. Env.js and associated.env,.env.testIs the environment variable associated with the test
  3. Package. json, updated some dependencies and Jest configuration items

Main environment: Vue2, Koa2, Nodejs V8.9.0

Some of the key dependencies used for testing

The following versions are the versions at the time of writing, or older

  1. Jest: ^ 21.2.1
  2. Babel – jest: ^ 21.2.0
  3. Supertest: ^ 3.0.0
  4. Dotenv: ^ 4.0.0

The rest of the dependencies can be project demo repository.

Set up the Jest test environment

I’m also new to testing. As for why I chose Jest over other frameworks (e.g. Mocha + Chai, Jasmine, etc.), I think I have my own opinion (you don’t have to use it, of course) :

  1. Developed by Facebook, the update speed and framework quality are guaranteed
  2. It has many integrated features (such as assertion libraries, such as test coverage)
  3. Complete documentation and easy configuration
  4. Support for typescript. I also used Jest to write tests while learning typescript
  5. Vue’s official unit testing framework, VUe-test-Utils, has specific testing instructions for Jest
  6. Support for snapshots is a big plus for front-end unit testing
  7. If you’re on the React stack, Jest is a natural fit for React

The installation

yarn add jest -D

#or

npm install jest --save-devCopy the code

Easy, right?

configuration

Since the Koa back end of my project is written in ES Modules instead of Commonjs for Nodejs, I need Babel plug-in for translation. Otherwise, when you run the test case, you will have the following problems:

When the Test suite failed to run/Users/molunerfinn/Desktop/work/web/vue - koa - demo /test/sever/todolist.test.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import _regeneratorRuntime from 'babel-runtime/regenerator'; import _asyncToGenerator from'babel-runtime/helpers/asyncToGenerator'; var _this = this; import server from'.. /.. /app.js';
                                                                                             ^^^^^^

    SyntaxError: Unexpected token import

      at ScriptTransformer._transformAndBuildScript (node_modules/jest-runtime/build/script_transformer.js:305:17)
          at Generator.next (<anonymous>)
          at new Promise (<anonymous>)Copy the code

Read the official Github README and see that babel-Jest is not installed.

yarn add babel-jest -D

#or

npm install babel-jest --save-devCopy the code

But strangely, the document says: babel-jest is automatically installed when installing Jest and will automatically transform files if a babel configuration exists in your project. Babel-jest is automatically installed when Jest is installed. This needs to be verified.

However, I found that the above problems still occurred when running test cases. After consulting relevant issues, I offered two solutions:

Babelrc env = babelrc env = babelrc env

1. Increase the presets

"Env" : {" test ": {" presets" : [" env ", "stage - 2"] / / the Babel - presents - env to translate}}Copy the code

2. Or add plugins

"Env ": {"test": {"plugins": ["transform-es2015-modules-commonjs"]Copy the code

Run it again and the compilation passes.

Normally we place the test files (*.test.js or *.spec.js) in the test directory of the project. Jest will run these test cases automatically. It is worth mentioning that we usually name the TDD based test file *.test.js and the BDD based test file *.spec.js. The difference between the two can be found in this article

We can add the test command to the package.json scripts field (change the name if it already exists, don’t conflict).

"scripts": { // ... "Test ": "jest" //... Other commands},Copy the code

This allows us to run NPM Test directly on the terminal to perform the test. Let’s start by writing back end Api tests.

Koa backend Api testing

Repeat the operation flow of the previous application. You can find that the application is in the before and after login state.

Tests can be written according to the flow or the structure of the back-end API. If you write tests according to the operation flow, they can be divided into before and after login. Based on the structure of the back-end API, you can write tests based on the structures and functions of routes or controllers.

Since the pre – and post-login apis in this example are basically separate, I wrote the tests based on the latter (routes or controllers) described above.

To explain the general (write) test steps:

  1. Write test specifications for each of your test specifications for what functionality was tested and what the expected results were.
  2. Write test body, usually input -> output.
  3. Judge the test results and compare the output with expectations. If the output matches expectations, the test passes. Otherwise, it does not pass.

Create a new server folder under the Test folder. Then create a user.spec.js file.

We can get through

import server from '.. /.. /app.js'Copy the code

To bring in the main entry file for our Koa application. But there was a problem. How do we make an HTTP request to this server and determine its return?

After reading Async Testing Koa with Jest and A Clear and Concise Introduction to Testing Koa with Jest and Supertest, I decided to use the supertest tool. It is a test tool specifically designed to test HTTP Server on the NodeJS side. It encapsulates the famous Ajax request library superAgent. And support for Promises means we can control the results of asynchronous requests with async await.

Installation:

yarn add supertest -D

#or

npm install supertest --save-devCopy the code

Let’s start writing our first test case. Let’s write one for login first. If we enter the wrong user name or password, we will not be able to log in. In the parameter returned by the backend, SUCCESS will be false.

// test/server/user.spec.js

import server from '.. /.. /app.js'
import request from 'supertest'

afterEach((a)= > {
  server.close() // When all tests have run, shut down the server
})

// If the user name is Molunerfinn and the password is 1234, you cannot log in. Correct should be Molunerfinn and 123.
test('Failed to login if typing Molunerfinn & 1234'.async() = > {// Async is used
  const response = await request(server) // Notice the use of await
                    .post('/auth/user') // The post method sends the following data to '/auth/user'
                    .send({
                      name: 'Molunerfinn'.password: '1234'
                    })
  expect(response.body.success).toBe(false) // Expect the success value of the body returned to be false (indicating login failure)
})Copy the code

In the example above, the test() method takes three arguments, the first a description of the test(string), the second a callback function (fn), and the third a delay parameter (number). There is no delay in this example. The expect() function then puts in the output and uses various match methods to compare the expected and output.

Running the NPM test at the terminal, nervously hoping to run through what might be life’s first test case. As a result, I get the following critical error message:

Low Post todolist failedif not give the params

    TypeError: app.address is not a function
 ...

 ● Post todolist failed if not give the params

    TypeError: _app2.default.close is not a functionCopy the code

What’s going on here? The server that we import does not have methods like close, address, etc. The reason is because of the last sentence in app.js:

export default appCopy the code

Here, export is an object. But we actually need a function.

During the Google process, two solutions were found:

Refer to solutions 1 and 2

1. Modify the app. Js

will

app.listen(8889, () = > {console.log(`Koa is listening in 8889`)})export default appCopy the code

Instead of

export default app.listen(8889, () = > {console.log(`Koa is listening in 8889`)})Copy the code

Can.

2. Modify your test file:

Callback () : server.callback() :

const response = await request(server.callback())
                    .post('/auth/user')
                    .send({
                      name: 'Molunerfinn'.password: '1234'
                    })Copy the code

I’m going to do the first one.

After the correction, pass smoothly:

 PASS  test/sever/user.test.js
  ✓ Failed to login if typing Molunerfinn & 1234 (248ms)Copy the code

However, there is a question: why is JEST still holding up the terminal process after the test is over? What I want is for JEST to exit automatically after testing. I checked the documentation and found that its CLI had a parameter –forceExit — that would solve this problem, so I modified the test command in package.json (we’ll change it several times later) and added this parameter:

"scripts": { // ... "Test ": "jest --forceExit" //... Other commands},Copy the code

I tested it again and found it was fine. This way we can continue to test the auth/* route:

// server/routes/auth.js

import auth from '.. /controllers/user.js'
import koaRouter from 'koa-router'
const router = koaRouter()

router.get('/user/:id', auth.getUserInfo) // The parameter that defines the URL is id
router.post('/user', auth.postUserAuth)

export default routerCopy the code

For example:

import server from '.. /.. /app.js'
import request from 'supertest'

afterEach((a)= > {
  server.close()
})

test('Failed to login if typing Molunerfinn & 1234'.async() = > {const response = await request(server)
                    .post('/auth/user')
                    .send({
                      name: 'Molunerfinn'.password: '1234'
                    })
  expect(response.body.success).toBe(false)
})

test('Successed to login if typing Molunerfinn & 123'.async() = > {const response = await request(server)
                    .post('/auth/user')
                    .send({
                      name: 'Molunerfinn'.password: '123'
                    })
  expect(response.body.success).toBe(true)
})

test('Failed to login if typing MARK & 123'.async() = > {const response = await request(server)
                    .post('/auth/user')
                    .send({
                      name: 'MARK'.password: '123'
                    })
  expect(response.body.info).toBe('User does not exist! ')
})

test('Getting the user info is null if the url is /auth/user/10'.async() = > {const response = await request(server)
                    .get('/auth/user/10')
  expect(response.body).toEqual({})
})

test('Getting user info successfully if the url is /auth/user/2'.async() = > {const response = await request(server)
                    .get('/auth/user/2')
  expect(response.body.user_name).toBe('molunerfinn')})Copy the code

It’s very simple and easy to understand. Look at the description + expectations and you’ll know what you’re testing. One thing to note, though, is that we’re using both toBe() and toEqual(). At first glance there seems to be no difference. There’s a big difference actually.

In short, toBe() fits the === criterion. For example 1 === 1, ‘hello’ === ‘hello’. But [1] === [1] is wrong. Specific reasons are not said, the basis of JS. So to check for things like arrays or objects that are equal you need toEqual().

OK, let’s test the API /* route.

Create a file called todolits.spec.js in the test directory:

With the experience of the last test, this shouldn’t be a problem. The server returns a 401 error if the JSON WEB TOKEN header is not included:

import server from '.. /.. /app.js'
import request from 'supertest'

afterEach((a)= > {
  server.close()
})

test('Getting todolist should return 401 if not set the JWT'.async() = > {const response = await request(server)
                    .get('/api/todolist/2')
  expect(response.status).toBe(401)})Copy the code

Everything seems to be fine, but the runtime error is reported:

console.error node_modules/jest-jasmine2/build/jasmine/Env.js:194 Unhandled error console.error node_modules/jest-jasmine2/build/jasmine/Env.js:195 Error: listen EADDRINUSE :::8888 at Object._errnoException (util.js:1024:11) at _exceptionWithHostPort (util.js:1046:20) at Server.setupListenHandle [as _listen2] (net.js:1351:14) at listenInCluster (net.js:1392:12) at Server.listen (net.js:1476:7) at Application.listen (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/node_modules/koa/lib/application.js:64:26) at Object.<anonymous> (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/app.js:60:5) at Runtime._execModule (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/node_modules/jest-runtime/build/index.js:520:13) at Runtime.requireModule (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/node_modules/jest-runtime/build/index.js:332:14)  at Runtime.requireModuleOrMock (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/node_modules/jest-runtime/build/index.js:408:19)Copy the code

It appears that two Koa instances are running at the same time, causing a listening port conflict. So we need to get Jest to execute sequentially. Looking at the official documentation, we see that the runInBand parameter is exactly what we want.

So modify the test command in package.json as follows:

"scripts": { // ... "Test ": "jest --forceExit --runInBand" //... Other commands},Copy the code

Run again, passed successfully!

Then there’s a problem. Our JWT token was originally generated and sent to the front end after a successful login. Today we test the API without going through the login step. Therefore, I think there are two ways to use tokens in the test:

  1. Add test time API interface, do not need to go throughkoa-jwtThe validation. However, this approach has an intrusive effect on the project, and there is a problem if we need to obtain information from tokens at some point.
  2. The backend generates a valid token in advance and then uses the token in the test. However, this method requires that the token cannot be disclosed.

I take the second option. I pre-generate a token and store it in a variable for readers’ convenience. (A real development environment would place the token of the test in the project environment variable. Env)

Next, let’s test the four operations of the database: add, delete, change and search. But to test all four interfaces at once, we can do it in this order: add, change, delete. You just add a todo and record the ID when you look it up. You can then update and delete with this ID.

import server from '.. /.. /app.js'
import request from 'supertest'

afterEach((a)= > {
  server.close()
})

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibW9sdW5lcmZpbm4iLCJpZCI6MiwiaWF0IjoxNTA5ODAwNTg2fQ.JHHqSDNUgg9YAFGWtD0 m3mYc9-XR3Gpw9gkZQXPSavM' // Pre-generated tokens

let todoId = null // The id used to store the toDO generated by the test

test('Getting todolist should return 401 if not set the JWT'.async() = > {const response = await request(server)
                    .get('/api/todolist/2')
  expect(response.status).toBe(401)})/ / to add
test('Created todolist successfully if set the JWT & correct user'.async() = > {const response = await request(server)
                    .post('/api/todolist')
                    .send({
                      status: false.content: 'From test'.id: 2
                    })
                    .set('Authorization'.'Bearer ' + token) // Add token authentication to header
  expect(response.body.success).toBe(true)})/ / check
test('Getting todolist successfully if set the JWT & correct user'.async() = > {const response = await request(server)
                    .get('/api/todolist/2')
                    .set('Authorization'.'Bearer ' + token)
  response.body.result.forEach((item, index) = > {
    if (item.content === 'From test') todoId = item.id / / to get id
  })
  expect(response.body.success).toBe(true)})/ / change
test('Updated todolist successfully if set the JWT & correct todoId'.async() = > {const response = await request(server)
                    .put(`/api/todolist/2/${todoId}/ 0 `) // Take the id to update
                    .set('Authorization'.'Bearer ' + token)
  expect(response.body.success).toBe(true)})/ / delete
test('Removed todolist successfully if set the JWT & correct todoId'.async() = > {const response = await request(server)
                    .delete(`/api/todolist/2/${todoId}`)
                    .set('Authorization'.'Bearer ' + token)
  expect(response.body.success).toBe(true)})Copy the code

We’ve tested them all against the API’s four main interfaces. Are we done testing the server side? It’s not. To ensure the robustness of the back-end API, we need to take many things into account. But it must be too tedious and mechanical to manually check every condition, statement and so on. So we need a metric to help us make sure that our testing is comprehensive. This is test coverage.

Back-end API test coverage

As mentioned above, Jest comes with test coverage (which is actually based on the Istanbul tool). How do you turn it on? And I made a lot of mistakes here.

By reading the official configuration documentation, I identified several parameters that need to be enabled:

  1. CoverageDirectory, which specifies the directory to output the test coverage report
  2. CoverageReporters, which specify the format of the output test coverage report, refer to the Istanbul instructions
  3. CollectCoverage, whether to collectCoverage information, of course.
  4. MapCoverage, since our code is translated by Babel-Jest, we need to turn on Sourcemap so that JEST can locate test results to the source code rather than the compiled code.
  5. Verbose displays whether each test case passes or not.

So we need to configure a Jest field in package.json (not in the scripts field, but at the same level as scripts) to configure Jest.

The configuration is as follows:

"jest": { "verbose": true, "coverageDirectory": "coverage", "mapCoverage": true, "collectCoverage": True, "coverageReporters": [" lCOv ", // Will generate LCOV test results and nice test coverage report in HTML format "text" // will output simple test report in command line interface]}Copy the code

Then we run the test again, and you can see that the terminal has printed a summary of the test report:

From this we can see that some fields are 100% and some are not. The last column played out is the code Lines that are not covered in the test. For a more visual view of the test results report, you can go to the root of the project and find a coverage directory. In the lCOv-Report directory there is an index.html for the output HTML report. Open it up and take a look:

The home page is an overview, much like the output from the command line. But we can take a deeper look and click on the directory provided by File on the left:

Then we can see that the number of lines of code not covered (50) and one function not tested:

Often the functions we don’t test are accompanied by lines of code that are not tested. We can see that in this case, app’s error event is not raised. Come to think of it, our tests are based on legitimate API requests. Therefore, the error event will not be raised. So we need to write a test case to test the.on(‘error’) function.

Often such test cases are not particularly easy to write. Fortunately, we can try to trigger an error on the server side. In this case, if a toDO is created to the server side without the corresponding information (ID, status, content), the toDO cannot be created and an error will be triggered.


// server/models/todolist.js

const createTodolist = async function (data) {
  await Todolist.create({
    user_id: data.id,
    content: data.content,
    status: data.status
  })
  return true
}Copy the code

Todo: todo: todo: todo: todo: todo: todo: todo: todo: todo

// test/server/todolist.spec.js
// ...
test('Failed to create a todo if not give the params'.async() = > {const response = await request(server)
            .post('/api/todolist')
            .set('Authorization'.'Bearer ' + token) // Do not send the created parameters
  expect(response.status).toBe(500) // The server reported a 500 error
})Copy the code

After testing, I found that the previous tests on app.js were 100%.

But controllers/todolist.js still has 34 untested rows, and we can see that the % Branch column shows 50 instead of 100. Branch stands for Branch testing. What is branch testing? In short, it’s your conditional statement test. Like an if… Else statement. If the test case only runs through the if condition, but not the else condition, then the Branch test is incomplete. Let’s see what condition is not tested?

You can see that it’s a ternary expression and it’s not fully tested. We tested the 0 case, but not the non-zero case, so write another non-zero case:

test('Failed to update todolist if not update the status of todolist'.async() = > {const response = await request(server)
                    .put(`/api/todolist/2/${todoId}/ 1 `) // <- the last argument is changed to 1
                    .set('Authorization'.'Bearer ' + token)
  expect(response.body.success).toBe(false)})Copy the code

Run the test again:

Ha! 100% test coverage!

Port occupancy and the introduction of environment variables

Although 100% test coverage was achieved, there was one problem that could not be ignored. The server listens on the same port in the test and development environments. This means that you cannot test your code in a development environment. For example, when you write a test case immediately after writing an API, if the test environment and the development environment server listen on the same port, the test will not listen because the port is occupied.

So we need to specify ports in the test environment that are different from ports in the development or even production environment. I started with the simple idea of specifying port 8888 for NODE_ENV=test and port 8889 for development. In app.js it looks like this:

// ...
let port = process.env.NODE_ENV === 'test' ? 8888 : 8889
// ...
export default app.listen(port, () => {
  console.log(`Koa is listening in ${port}`)})Copy the code

Then there are two problems:

  1. The cross-platform env setup needs to be addressed
  2. So if you set it this way, once you’re in the test environment, for port,BranchTests cannot pass completely — they are always in the test environment and cannot be run toport = 8889The conditions of

Cross-platform Env Settings

Cross-platform EnVs mainly involve Windows, Linux, and macOS. To run NODE_ENV=test on all three platforms, we need cross-env to help us.

yarn add cross-env -D

#or

npm install cross-env --save-devCopy the code

Then modify test in package.json as follows:

"scripts": { // ... Other commands "test": "cross-env NODE_ENV=test jest --forceExit --runInBand" //... Other commands},Copy the code

This allows us to access the test value in the backend code using the process.env.node_env variable. That solves the first problem.

Port separation and guaranteed test coverage

So far, we have been able to solve the problem of consistent listening ports in the test and development environments. However, this has led to the problem of incomplete test coverage.

I came up with two solutions:

  1. The Istanbul special ignore comment ignores some of the test branch conditions in the test environment
  2. You can configure environment variable files to use different environment variable files in different environments

The first method is simply to ignore the code by typing /* Istanbul ignore next */ or /* Istanbul ignore

[non-word] [optional-docs] */ and so on. However, considering that this involves environment variables in both the test environment and the development environment, if it is more than just a port issue, the second approach is more elegant. (For example, if the database user and password of the development environment and test environment are different, the corresponding environment variable should be written)

At this point we need another popular library, Dotenv, which reads the values in the.env file by default, so that our project can handle different environmental requirements through different.env files.

The steps are as follows:

1. Install dotenv
yarn add dotenv

#or

npm install dotenv --saveCopy the code
2. Create the project in the root directory.envand.env.testTwo files, one for development environment and one for test environment

// .env

DB_USER=xxxx # database user
DB_PASSWORD=yyyy # database password
PORT=8889 # monitor portCopy the code

// .env.test

DB_USER=xxxx # database user
DB_PASSWORD=yyyy # database password
PORT=8888 # monitor portCopy the code
3. Create oneenv.jsFile for different environments using different environment variables. The code is as follows:
import * as dotenv from 'dotenv'
let path = process.env.NODE_ENV === 'test' ? '.env.test' : '.env'
dotenv.config({path, silent: true})Copy the code
4. Introduce env at the beginning of app.js
import './env'Copy the code

Then change the original sentence “port” to:

let port = process.env.PORTCopy the code

Replace the password of the user connected to the database with the environment variable:

// server/config/db.js

import '.. /.. /env'
import Sequelize from 'sequelize'

const Todolist = new Sequelize(`mysql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@localhost/todolist`, {
  define: {
    timestamps: false // Disable Sequelzie to automatically timestamp data tables (createdAt and updatedAt)}})Copy the code

Env and.env.js files should not be included in git repositories because they are important.

So you can use different variables in different environments. Slow! Doesn’t that solve the problem? Conditions in env.js still can’t be overridden by tests – you might be wondering. Don’t worry, here’s the solution — give Jest the scope to collect test coverage:

Modify the jest field in package.json as follows:

"jest": { "verbose": true, "coverageDirectory": "coverage", "mapCoverage": true, "collectCoverage": true, "coverageReporters": [ "lcov", "text" ], "collectCoverageFrom": [/ / specified Jest to collect test coverage range "! Env. Js ", / / exclude env. Js "server / / *. * * js", "app. Js]}Copy the code

After you’ve done that, run the test again and pass:

This completes our back-end API testing. 100% test coverage was achieved. Now we are ready to test Vue’s front-end project.

Vue front-end test

Vue front-end tests I would recommend from the official vue-test-utils. Of course, front-end testing can be roughly divided into Unit test and e2E test. Because end-to-end testing has strict requirements on the testing environment and is tedious, and the official testing framework is the Unit testing framework, Therefore, this article only introduces unit tests with official tools for Vue front-end testing.

In the front-end test of Vue, we can learn the features and usages of Jest such as mock and snapshot, and a series of operations such as mount, shallow and setData provided by VUe-test-utils.

Install the vue – test – utils

According to the official website, we need to install the following:

yarn add vue-test-utils vue-jest jest-serializer-vue -D

#or

npm install vue-test-utils vue-jest jest-serializer-vue --save-devCopy the code

Vue-test-utils is the most critical testing framework. Provides a series of test operations for Vue components. (mentioned below). Vue-jest is used to process *. Vue files. Jest-serializer -vue is used to provide snapshot serialization for snapshot tests.

Configure vue-test-utils and jEST

1. Modify. Babelrc

Add or modify presets to test env:

{
  "presets": [["env", { "modules": false}]."stage-2"]."plugins": [
    "transform-runtime"]."comments": false."env": {
    "test": {
      "plugins": ["transform-es2015-modules-commonjs"]."presets": [["env", { "targets": { "node": "current"}}] // Add or modify]}}}}Copy the code

2. Modify the jest configuration in package.json:

"Jest" : {" verbose ": true," moduleFileExtensions: "[]" js ", "transform" : {/ / increase the transform conversion ". * \ \ (vue) $" : "<rootDir>/node_modules/vue-jest", "^.+\\.js$": "<rootDir>/node_modules/babel-jest" }, "coverageDirectory": "coverage", "mapCoverage": true, "collectCoverage": true, "coverageReporters": [ "lcov", "text" ], "moduleNameMapper": {/ / processing webpack alias "@ / (. *) $" :" < rootDir > / SRC / $1 "}, "snapshotSerializers" : [// Configure a snapshot test "<rootDir>/node_modules/jest-serializer-vue"], "collectCoverageFrom": [ "!env.js", "server/**/*.js", "app.js" ] }Copy the code

Some instructions for front-end unit testing

For vue-test-utils and Jest compatibility testing, I recommend checking out this series of articles for clarity.

Next, figure out what front-end unit tests need to test. To quote vue-test-utils:

For UI components, we do not recommend pursuing line-level coverage because it can lead us to focus too much on the internal implementation details of the component, leading to trivial testing.

Instead, we recommend writing tests as public interfaces that assert your component and handling them inside a black box. A simple test case will assert whether some input (user interaction or prop change) provided to a component results in the desired result (render result or trigger custom event).

For example, for a Counter component that increments the count by one each time a button is clicked, its test case will simulate the click and assert that the render result will increase by one. The test does not focus on how Counter increments, but only on its inputs and outputs.

The advantage of this proposal is that even if the component’s internal implementation has changed over time, the test will pass as long as your component’s public interface remains consistent.

Therefore, front-end unit testing is not necessarily about test coverage as much as back-end API testing is about test coverage. (Of course, it’s ok to achieve 100% test coverage, but you’ll need to write a lot of tedious test cases that cost you more time than you can get.) Instead, we just need the basics of regression testing: given an input, I only care about the output, not how it’s implemented internally. As long as you can cover user-specific operations, you can test the functionality of the page.

As before, we write our test cases in the test/client directory. For unit testing of Vue, we are testing against *.vue files. Since app.vue in this example is meaningless, just test login. vue and todolist. vue.

How do you test with vue-test-utils? In short, all we need to do is render the component in the back end using the mount or shallow method provided by vue-test-utils, and then simulate the user’s actions or our test conditions using methods such as setData, propsData, setMethods, etc. Finally, expect assertions provided by JEST are used to judge the expected results. There’s a lot of expectation here. We can test our components more thoroughly by determining whether events are fired, whether elements exist, whether data is correct, whether methods are invoked, and so on. They are also covered more fully in the following examples.

The Login. Vue test

Create a login.spec.js file.

First let’s test if the page has two input fields and a login button. According to the official document, I first noticed shallow Rendering, which explains that, for a component, only render the component itself instead of its sub-components, so as to improve the testing speed and conform to the concept of unit testing. It looks pretty good. Bring it here.

Find element test

import { shallow } from 'vue-test-utils'
import Login from '.. /.. /src/components/Login.vue'

let wrapper

beforeEach((a)= > {
  wrapper = shallow(Login) Make sure our test instances are clean and complete before each test. Return a Wrapper object
})

test('Should have two input & one button', () = > {const inputs = wrapper.findAll('.el-input') // Find dom or vue instances with findAll
  const loginButton = wrapper.find('.el-button') // Use find to find elements
  expect(inputs.length).toBe(2) There should be two input boxes
  expect(loginButton).toBeTruthy() // There should be a login button. ToBeTruthy passes as long as it asserts that the condition is not empty or false.
})Copy the code

Everything seemed normal. Run the tests. The result was wrong. Input. Length does not equal 2. The debug breakpoint shows that the element was not found.

What’s going on here? Oh, and as I recall, the el-input and el-button shapes are also child components, so shallow doesn’t render them. In this case, shallow is not appropriate. So you still need to render with mount, which will render the page as it should.

import { mount } from 'vue-test-utils'
import Login from '.. /.. /src/components/Login.vue'

let wrapper

beforeEach((a)= > {
  wrapper = mount(Login) Make sure our test instances are clean and complete before each test. Return a Wrapper object
})

test('Should have two input & one button', () = > {const inputs = wrapper.findAll('.el-input') // Find dom or vue instances with findAll
  const loginButton = wrapper.find('.el-button') // Use find to find elements
  expect(inputs.length).toBe(2) There should be two input boxes
  expect(loginButton).toBeTruthy() // There should be a login button. ToBeTruthy passes as long as it asserts that the condition is not empty or false.
})Copy the code

Test, or error! They still haven’t been found. Why is that? Think again. We didn’t introduce Element-UI into our tests. Because. El-input is actually a component of elemental-UI, vue cannot render an el-input as

without it. We’ll be fine when we figure it out. Bring it in. Because our project had a main.js entry file in the Webpack environment, we didn’t have it in the test. Therefore, Vue naturally does not know what dependencies you used in the test, so we need to introduce them separately:

import Vue from 'vue'
import elementUI from 'element-ui'
import { mount } from 'vue-test-utils'
import Login from '.. /.. /src/components/Login.vue'

Vue.use(elementUI)

// ...Copy the code

Run the test again and pass!

A snapshot of the test

Next, use one of the great built-in features of Jest: Snapshots. It can store the HTML structure of a certain state in the form of a snapshot file. If the result of each snapshot test is inconsistent with the previous snapshot test, the test cannot pass.

Of course, if the page really needs to be changed in the future and the snapshot needs to be updated, you only need to add a -u parameter when executing jest to realize the snapshot update.

So let’s put it into practice. For the landing page, we really just need to make sure that the HTML structure is fine so that all the necessary elements are there. So snapshot tests are particularly handy to write:

test('Should have the expected html structure', () => {
  expect(wrapper.element).toMatchSnapshot() // Call toMatchSnapshot to compare snapshots
})Copy the code

If this is your first snapshot test, it will create a __snapshots__ directory to store the snapshot files in your test file’s directory. The above test generates a login.spec.js.snap file as follows:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Should have the expected html structure 1`] = `
<div
  class="el-row content"
>
  <div
    class="el-col el-col-24 el-col-xs-24 el-col-sm-6 el-col-sm-offset-9"
  >
    <span
      class="title"
    >Welcome to login</span>

    <div
      class="el-row"
    >
      <div
        class="el-input"
      >
        <! ---->
        <! ---->
        <input
          autocomplete="off"
          class="el-input__inner"
          placeholder="Account"
          type="text"
        />
        <! ---->
        <! ---->
      </div>

      <div
        class="el-input"
      >
        <! ---->
        <! ---->
        <input
          autocomplete="off"
          class="el-input__inner"
          placeholder="Password"
          type="password"
        />
        <! ---->
        <! ---->
      </div>

      <button
        class="el-button el-button--primary"
        type="button"
      >
        <! ---->
        <! ---->
        <span>The login</span>
      </button>
    </div>
  </div>
</div>
`;Copy the code

You can see that it saves the entire HTML structure as a snapshot. Snapshot testing ensures the integrity and stability of our front-end page structure.

The methods to test

Many times we need to test whether some of the methods in the Vue can be triggered under certain circumstances. For example, in this case, clicking the login button should trigger the loginToDo method. Then it comes to the test of methods. At this point, setMethods provided by VUe-test-utils is very useful. We can set (override) the loginToDo method to see if it is triggered.

Note that once setMethods are used, your new function completely overrides the original function inside a test(). The same goes for other methods called this.xxx(), including the Vue instance.

test('loginToDo should be called after clicking the button', () = > {const stub = jest.fn() // Mock a jest
  wrapper.setMethods({ loginToDo: stub }) // setMethods override the loginToDo method
  wrapper.find('.el-button').trigger('click') // Trigger a click event on the button
  expect(stub).toBeCalled() // Check whether loginToDo is called
})Copy the code

Note that we used the jest. Fn method here, which will be explained in the next section. All you need to know here is that this is a method provided by Jest that can be used to check whether it has been called.

Mock method testing

The next step is to test the login function. Since we tested the Koa back-end API, we could default to the back-end API to return the correct results in our front-end tests. (This is why we tested the Koa side first, ensuring the robustness of the back-end API so that we could easily go back to the front-end testing.)

That makes sense, but how do we default to, or “fake”, our API requests and the data returned? This is where a useful feature of Jest comes in. It’s safe to say that the word mock is familiar to many people who do front-end work. In the absence of a back end, or in the absence of back-end functionality, we can mock the API to mock requests and data.

Jest’s mock is the same, but more powerful is that it can fake libraries. Take axios, the HTTP request library we’re going to use next. For our page, the login simply needs to send a POST request and determine if success is returned true. Let’s start by mocking axios and its POST request.

jest.mock('axios', () = > ({post: jest.fn((a)= > Promise.resolve({
    data: {
      success: false.info: 'User does not exist! '}}}))))Copy the code

Then we can introduce Axios into our project:

import Vue from 'vue'
import elementUI from 'element-ui'
import { mount } from 'vue-test-utils'
import Login from '.. /.. /src/components/Login.vue'
import axios from 'axios'

Vue.use(elementUI)

Vue.prototype.$http = axios

jest.mock(....)Copy the code

Mock () is written under import axios from ‘axios’, so doesn’t that mean axios is imported from node_modules? Mock () will implement the function promotion, which means that the code above is actually the same as the code below:

jest.mock(....)
import Vue from 'vue'
import elementUI from 'element-ui'
import { mount } from 'vue-test-utils'
import Login from '.. /.. /src/components/Login.vue'
import axios from 'axios' // The axios here is axios from jest. Mock ()

Vue.use(elementUI)

Vue.prototype.$http = axiosCopy the code

It even looks like there’s a bit of a boost to var.

The benefit is obvious, though, and we can achieve the same goal using the first version without breaking ESLint’s rules.

Then you’ll also notice that we used the jest.fn() method, which is an important part of jest’s mock method. It is itself a mock function. It enables tracing of method calls as well as simulation capabilities to create complex behaviors as discussed later.

Continuing the quiz we didn’t finish writing:

test('Failed to login if not typing the correct password'.async () => {
  wrapper.setData({
    account: 'molunerfinn'.password: '1234'
  }) // Simulate user input data
  const result = await wrapper.vm.loginToDo() // Simulate the effect of an asynchronous request
  expect(result.data.success).toBe(false) // Expect to return data where SUCCESS is false
  expect(result.data.info).toBe('Wrong password! ')})Copy the code

We use setData to simulate the user entering data in two input fields. The loginToDo method is then explicitly called via wrapper.vm. LoginToDo (). Since we are returning a Promise object, we can use async await to get the resolve data out. Then test if it matches expectations. We tested the input error this time, and the test passed without any problems. What if I want to test the user’s password again? Our mock axios has only one post method, why can one method output multiple results? The next section explains this in more detail.

Create complex behavior tests

Let’s review our mock:

jest.mock('axios', () = > ({post: jest.fn((a)= > Promise.resolve({
    data: {
      success: false.info: 'User does not exist! '}}}))))Copy the code

As you can see, a POST request can always return only one result. How can I mock this POST method and implement multiple result tests? Next comes Jest’s other killer method: mockImplementationOnce. The official example is as follows:

const myMockFn = jest.fn((a)= > 'default')
  .mockImplementationOnce((a)= > 'first call')
  .mockImplementationOnce((a)= > 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'Copy the code

Calling the same method four times gives different results. This is exactly what we want.

So we need to rewrite our axios mock method when we test the successful login method:

jest.mock('axios', () = > ({post: jest.fn()
        .mockImplementationOnce((a)= > Promise.resolve({
          data: {
            success: false.info: 'User does not exist! '
          }
        }))
        .mockImplementationOnce((a)= > Promise.resolve({
          data: {
            success: true.token: 'xxx' // Optionally return a token}}}))))Copy the code

Then start writing our test:

test('Succeeded to login if typing the correct account & password'.async () => {
  wrapper.setData({
    account: 'molunerfinn'.password: '123'
  })
  const result = await wrapper.vm.loginToDo()
  expect(result.data.success).toBe(true)})Copy the code

Just when I thought it was the same as the previous test, an error came in. Here’s what loginToDo is doing when success is true:

if (res.data.success) { // If successful
  sessionStorage.setItem('demo-token', res.data.token) // Use sessionStorage to store tokens
  this.$message({ // Successful login, a message is displayed
    type: 'success'.message: 'Login successful! '
  })
  this.$router.push('/todolist') // The todolist page is displayed, and the login succeeds
}Copy the code

It didn’t take long for me to see what was wrong: We didn’t have sessionStorage in our test environment, which should have been on the browser side. And we are not using vue-router, so we cannot execute this.$router.push().

With regard to the former, it is easy to find a solution to the problem.

Start by installing the mock-local-storage library (also includes sessionStorage)

yarn add mock-local-storage -D

#or

npm install mock-local-storage --save-devCopy the code

Then configure the jest parameter in package.json:

"jest": {
  // ...
  "setupTestFrameworkScriptFile": "mock-local-storage"
}Copy the code

For the latter, having read the official advice, we should not introduce vue-Router, which would break our unit tests. We can mock it accordingly. This mocks feature is used in vue-test-utils:

const $router = { Declare a $router object
  push: jest.fn()
}

beforeEach((a)= > {
  wrapper = mount(Login, {
    mocks: {
      $router // Mount into mount mocks in beforeEach hook.}})})Copy the code

In this way, the $router object will be attached to the instance’s prototype, which will be called within the component as this.$router.push().

After solving the above two problems, we also passed the test smoothly:

Next, test todolist. vue.

Todolist. Vue test

Keyboard event testing and implicit event firing

Similarly, we create a file called todolist.spec.js in the test/client directory.

Let’s start with some of the environments from the above example:

import Vue from 'vue'
import elementUI from 'element-ui'
import { mount } from 'vue-test-utils'
import Todolist from '.. /.. /src/components/Todolist.vue'
import axios from 'axios'

Vue.use(elementUI)

jest.mock(...) // Follow up

Vue.prototype.$http = axios

let wrapper

beforeEach((a)= > {
  wrapper = mount(Todolist)
  wrapper.setData({
    name: 'Molunerfinn'.// Preset data
    id: 2})})Copy the code

Let’s start with a simple test to see if the data is correct:

// test 1
test('Should get the right username & id', () => {
  expect(wrapper.vm.name).toBe('Molunerfinn')
  expect(wrapper.vm.id).toBe(2)})Copy the code

It is important to note that the Todolist page fires the getUserInfo and getTodolist methods at the Created stage, while our wrapper is after the Mounted stage. So by the time we get the Wrapper, created, Mounted and other life-cycle hooks are already running. In this example, getUserInfo is taken from sessionStorage, and ajax requests are not involved. But getTodolist involves requests, so you need to configure it in the jest. Mock method, otherwise you will get an error:

jest.mock('axios', () = > ({get: jest.fn()
        // for test 1
        .mockImplementationOnce((a)= > Promise.resolve({
          status: 200.data: {
            result: []}}))})Copy the code

The above mentioned getTodolist and getUserInfo are implicit events that need to be noted during testing. They are not controlled by your test and are fired in the component.

Let’s start testing keyboard events. Similar to mouse events, keyboard events are named after the event. For some common events, however, vue-test-utils provides aliases such as:

Enter, TAB, delete, ESC, space, up, Down, left, right You can write tests like this:

const input = wrapper.find('.el-input')
input.trigger('keyup.enter')Copy the code

Of course, if you need to specify a key, you can also provide keyCode:

const input = wrapper.find('.el-input')
input.trigger('keyup', {which: 13 // Enter's keyCode is 13
})Copy the code

So let’s refine this test to see if addTodos is triggered when I press enter while the input field is active:

test('Should trigger addTodos when typing the enter key', () = > {const stub = jest.fn()
  wrapper.setMethods({
    addTodos: stub
  })
  const input = wrapper.find('.el-input')
  input.trigger('keyup.enter')
  expect(stub).toBeCalled()
})Copy the code

No problem, one pass.

Note that we need to add the. Native modifier to call native events on components in real development:

<el-input placeholder="Please enter your to-do list" v-model="todos" @keyup.enter.native="addTodos"></el-input>Copy the code

However, in vue-test-utils you can trigger it directly using the native Keyup. enger.

The use of wrapper. The update ()

A lot of times we’re dealing with asynchrony. In particular, asynchronous values, asynchronous assignments, asynchronous page updates. There are far too many asynchronous cases for actual development using Vue.

Remember nextTick? Most of the time, we need to retrieve the result of a change from this. XXX instead of this.$nextTick(). In our tests we will encounter many cases where we need to fetch asynchronously, but we don’t need nextTick and can update components with async await in conjunction with wrapper.update() instead. For example, the following test adds todo successfully:

test('Should add a todo if handle in the right way'.async () => {
  wrapper.setData({
    todos: 'Test'.stauts: '0'.id: 1
  })

  await wrapper.vm.addTodos()
  await wrapper.update()
  expect(wrapper.vm.list).toEqual([
    {
      status: '0'.content: 'Test'.id: 1})})Copy the code

In this case, the steps from entering the page to adding a todo and displaying it are as follows:

  1. getUserInfo -> getTodolist
  2. Type todo and hit Enter
  3. addTodos -> getTodolist
  4. Displays the added TODo

You can see that there are three Ajax requests in total. The first step is outside the scope of our test(), 2, 3 and 4 are all within our control. The two Ajax requests, addTodos and getTodolist, enable asynchronous operations. Although we mock the method, we essentially return a Promise object. So still need to wait with await.

Note that you add the corresponding mockImplementationOnce get and POST requests to jest.mock().

So the first step is to await wrapper.vm.addtodos (). The second step, await wrapper.update(), is actually waiting for getTodolist to return.

Both are indispensable. After two steps we can test if we have the information for the returned TODO by asserting the data list.

The following is some of the toDO add, delete, change and check operations, using the test method has been similar to the above mentioned, no further details. This concludes all the independent test case descriptions. Look at the sense of accomplishment of passing this test:

However, I also want to share some experience about debugging in the test. Cooperating with debugging can better judge the unpredictable problems occurred in our test.

Use VSCode to debug tests

Since I use VSCode for my own development and debugging, those of you who use other ides or editors may be disappointed. But don’t worry, consider joining VSCode!

Nodejs version 8.9.0 and VSCode version 1.18.0 were used at the time of writing this article, so all debug test configurations are only guaranteed to be applicable to the current environment. Other environments may need to be tested on their own without further ado.

The configuration for jest debugging is as follows :(note that the configuration path is VScode for this project.vscode /launch.json)

{ // Use IntelliSense to learn about possible Node.js debug attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0," "configurations" : [{" name ":" the Debug Jest ", "type" : "node", "request" : "launch", "the program" : "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", "stopOnEntry": false, "args": [ "--runInBand", "--forceExit" ], "cwd": "${workspaceRoot}", "preLaunchTask": null, "runtimeExecutable": null, "runtimeArgs": [ "--nolazy" ], "env": { "NODE_ENV": "test" }, "console": "integratedTerminal", "sourceMaps": true } ] }Copy the code

After configuring the above configuration, you can find an option called DEBUG Jest in the DEBUG panel (don’t tell me you don’t know what the DEBUG panel is ~) :

Then you can break points in your test file:

Then run debug mode and press the green start button to go into Debug mode and stop at the breakpoint:

In the left pane, you can find the value of the variable, the type of the variable, and so on for the current scope under Local and Closure. Make full use of VSCode debug mode, the development of error detection and debugging efficiency will greatly increase.

conclusion

This article describes at length how to set up a Jest test environment and improve our test environment during the test process. It describes the method of Koa back-end testing and the improvement of test coverage, describes the Vue front-end unit testing environment and the construction of many corresponding test instances, as well as in the process of testing constantly encountered problems and solve problems. Those who can see here are not generally patient people, applaud for you ~ also hope that through this article you can experience and feel in your heart several key points proposed at the beginning of this article:

You can learn about the meaning of testing, the construction of Jest testing framework, the similarities and differences of front and back end testing, how to write test cases, how to view test results and improve our test coverage, whether 100% test coverage is necessary, and various difficulties encountered in the construction of test environment and testing itself.

All of the test cases and overall project examples for this article can be found in my Vue-Koa-Demo github project. If you like my article and project, please click star. If you have any suggestions or comments on my article and project, please feel free to comment at the end of this article or discuss with me in the issues of this project!

This article first appeared in my blog, welcome to check out ~

Refer to the link

Koa related

Supertest with KOA error

Exit automatically after the test

Async testing Koa with Jest

How to use Jest to test Express middleware or a funciton which consumes a callback?

A clear and concise introduction to testing Koa with Jest and Supertest

Debug jest with vscode

Test port question

Coverage bug

Eaddrinuse bug

Istanbul ignore

Vue related

vue-test-utils

Test Methods and Mock Dependencies in Vue.js with Jest

Storage problem