Through an article Front end test automation – introductory article, believe everybody with front end test automation have a preliminary knowledge and understanding of this section, we will have a thorough understanding of the unit tests, in Jest framework, for example, from theory to practice comprehensive unit testing (finish see this, don’t have to worry about the interviewer asks questions about unit testing).

Basic introduction and configuration

Jest is an open source JavaScript testing framework from Facebook. Compared with other testing frameworks, one of the major features is the built-in common testing tools, such as zero configuration, built-in assertions, test coverage tools and other functions, to achieve out-of-the-box.

Jest works for, but is not limited to, projects that use Babel, TypeScript, Node, React, Angular, Vue, etc.

Jest main features:

  • Zero configuration
  • Bring their own assertion
  • As a front-end testing framework, Jest can use its unique snapshot testing function to automatically test common front-end frameworks such as React by comparing the snapshot files generated by UI codes.
  • In addition, Jest’s test cases are executed in parallel and only the tests for the changed files are executed, increasing testing speed.
  • Test coverage
  • The Mock simulation

These characteristics, we can simply understand, the subsequent practice will have a more in-depth introduction and use.

Project initialization

  1. Create a project
mkdir jest-demo
cd jest-demo
npm init
Copy the code
  1. Install the jest
npm i jest --save-dev
Copy the code
  1. Execute test command

Configure the jest command in package.json

// package.json
// ...
scripts: {
    "test": "jest --watchAll",
}
Copy the code

Then execute NPM run test to test all files ending in *.test.js.

Set up the Jest configuration

There are two ways to set up the JEST configuration:

  1. Generate the JEST configuration file separately via jEST –init
  2. Set named line parameters on the CLI.

Generate the configuration file separately

jest --init
Copy the code

Using this command, you can generate the jest.config.js configuration file, some of which can be modified globally.

Using CLI options

In addition to configuring related attributes through the configuration file, you can also specify related parameters when running commands. Such as:

NPX jest 01/demo.test.js --watchAll // Tests specified filesCopy the code

Here we explain: -Watchall property

Jest --watchAll // monitor all files directly jest --watch // monitor all files directlyCopy the code

The running results are as follows:

In addition, note some additional monitoring options in the figure above: f, O, etc. In fact, in order to more quickly monitor the changes of the file, in the actual development of time after you can master.

Use the ES6 module

By default, the modules to be tested can only be imported through the commonJS specification in the test.js test file, if we want to use ES6 modules, we need to do some extra configuration.

  1. Install babel-related dependencies
npm i babel-jest @babel/core @babel/preset-env --save-dev
Copy the code
  1. Add the Babel. Config. Js
// babel.config.js
module.exports = {
  presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
Copy the code

We can then use the ES6 module in the.test.js test script.

Jest works with Babel: Before running a test, use Babel to convert your code, convert the module to CommonJS, and run the converted test case code. The specific process is as follows:

  • npm run jest
  • jest
  • babel-jest
  • babel-core
  • Babel configuration file
  • Convert ES5
  • Execute the transformed test code

Preliminary experience

First, let’s take a look at how to write unit tests through Jest with a simple example.

For example, we define two functions, sum and subtract. How do we add write test cases to them?

// demo.js
function sum(a, b) {
  return a + b;
}
function subtract(a, b) {
  return a - b;
}

module.exports = {
  sum,
  subtract,
};
Copy the code
// demo.test.js
const { sum, subtract } = require("./demo");

test("sum: ", () => {
  expect(sum(1, 2)).toBe(3);
});

test("subtract: ", () => {
  expect(subtract(1, 2)).toBe(-1);
});
Copy the code

At this point, the functions to be tested and the test scripts have been written. Next, we need to install the JEST dependencies.

npm i jest --save-dev
Copy the code

Then, configure script to run the script

{
	 "scripts": {
    "test": "jest"
  },
  //...
}
Copy the code

By executing NPM run test, Jest will automatically find all files under the current project that end in.test.js and execute the test script.

Note: We do not manually import jest in our test script, but use test, expect, etc. This is because these methods are automatically injected into the.test.js file when jest is executed, so we do not need to import them manually.

At this point, there may be a question, if we do not manually introduce the dependency package, then we call its API intelligent prompt is not there, it is not there, in this case, we can install the following dependency to solve the problem of intelligent prompt

NPM i@types /jest --save-dev // Note that the installation must be in the project root directory.Copy the code

Global API

Jest object

After we install JEST and execute the jest command, the executed test file automatically creates the JEST global object.

Methods in jEST objects help create models and let you control the overall behavior of jEST. It can also be introduced by importing {jest} from ‘@jest/globals’.

The internal properties of jEST objects can be directly referred to the official website – Jest objects.

The test function

The test function, the core of unit testing, helps us create a test case, and a unit test is composed of test cases.

Test (' specify function points to test ', () => {//... })Copy the code

Expect function

There’s a new term here: an assertion, which is the expect function that tells us whether something actually runs as expected. Since we are testing existing code, we are actually comparing the results of the actual run with the results we expect. If they are consistent, the test passes.

expect(1).toBe(1); 
Copy the code

The describe function

The Test function can be used to create individual test cases. As more and more test cases are created, we need to categorize test cases. This is the purpose of the Describe function.

Describe (test 1, () = > {test (' test case 1-1, () = > {/ /... }) test(' test case 1-2', () => {//... })}) go in (' 2 'test, () = > {test (' test cases 2-1, () = > {/ /... }) test(' test case 2-2', () => {//... })})Copy the code

In actual development, describe and test are both required functions.

Lifecycle hook functions

As the name implies, we have hook functions before and after each test case execution to do some intercepting work.

BeforeAll // all test cases are executed before execution, only once beforeEach // is executed beforeEach test case execution, may be executed multiple times afterEach // is executed afterEach test case execution, AfterAll // may be executed multiple times afterAll test cases are executed, but only once.Copy the code

In addition, these hook functions can also be used in the describe function.

// counter.js export default class Counter { constructor() { this.number = 0; } addOne(){ this.number += 1; } addTwo(){ this.number += 2; } minusOne() { this.number -= 1; } minusTwo() { this.number -= 2; }}Copy the code
// test.counter.js import Counter from './counter' let counter = null; beforeAll(() => { console.log('beforeAll') }) afterAll(() => { console.log('afterAll'); }) beforeEach(() => { console.log('beforeEach') counter = new Counter(); }) afterEach(() => {console.log('afterEach')}) describe( () => { beforeAll(() => { console.log('beforeAll add test') }) afterAll(() => { console.log('afterAll add test'); }) beforeEach(() => { console.log('beforeEach add test') }) afterEach(() => { console.log('afterEach add test') }) Test.only (' test addOne method on Counter ', () => {console.log(' test addOne method on Counter ') counter.addone (); expect(counter.number).toBe(1); }) test(' test addTwo method for Counter ', () => {console.log(' test addTwo method for Counter ') counter.addtwo (); expect(counter.number).toBe(2); })}) describe(' test minus code ', () => {test(' test minusOne method ', () => {console.log(' test minusOne method on Counter '); counter.minusOne(); expect(counter.number).toBe(-1); }) test(' test minusTwo method on Counter ', () => {console.log(' test minusTwo method on Counter '); counter.minusTwo(); expect(counter.number).toBe(-2); })})Copy the code

The first test case in the above code is executed in the following order:

There are two additional points to note here:

  1. If we want to execute only the current test case for the time being, we can use test.only()
  2. Lifecycle hook functions are scoped, that is, only for test cases within Describe where the lifecycle hooks are. These hook functions are not available in other describe test cases.

The matcher is commonly used

Jest provides a number of matcher apis that can help us quickly determine whether a program’s results are what we expect.

Here, we have sorted out different data types:

Boolean value

  • toBeNull
  • toBeDefined
  • toBeUndefined
  • toBeTruthy
  • toBeFalsy
test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});
Copy the code

string

  • toMatch()
The test (' testing string, () = > {expect (" hello world "). ToMatch (/ world /); });Copy the code

The numerical

  • toBe
  • ToEqual: equal to
  • ToBeGreaterThan: greater than
  • ToBeGreaterThanOrEqual: greater than or equal to
  • ToBeLessThan: less than
  • ToBeLessThanOrEqual: less than or equal to
  • ToBeCloseTo: Comparison of floating point numbers
Test (' test ', () => {const value = 4; expect(value).toBeGreaterThan(3); expect(value).toBeGreaterThanOrEqual(4); expect(value).toBeLessThan(5); expect(value).toBeLessThanOrEqual(5); expect(value).toBe(4); expect(value).toEqual(4); Expect (0.1 + 0.2) toBeCloseTo (0.3); })Copy the code

Arrays and collections

  • toContain
Test (' test array and set ', () => {const arr = ['kobe', 'James ', 'durant'] expect(arr). ToContain ('kobe'); expect(new Set(arr)).toContain('kobe'); })Copy the code

abnormal

  • toThrow
The test (' abnormal test, () = > {expect (() = > fn ()). ToThrow (); expect(() => fn()).toThrow(Error); expect(() => fn()).toThrow('this is a error'); expect(() => fn()).toThrow(/error/); })Copy the code

The above are some common matchers, more matchers, can refer to: official website – matchers

Testing asynchronous code

In the preliminary experience, the function under test is a simple synchronous function with clear input and output, which is very simple to write, but in normal development, it is obviously impossible to be so simple, and there will definitely be various asynchronous codes, such as timer, Ajax request, etc.

So how do you test asynchronous code? Here we have two cases:

  1. Asynchronous code in callback form
  2. Asynchronous code in the form of promise

callback

  1. First, let’s take a look at the asynchronous code that executes through a callback.
// fetchData.js import axios from 'axios'; export const fetchData = (fn) => { return axios.get('http://www.dell-lee.com/react/api/demo1.json').then(response => { fn(response.data) }) } // fetchData.test.js import {fetchData} from './fetchData'; // Error: This test case will pass with or without an interface error, because the incoming callback function itself is not executed. Test ('fetchData returns result {success: true}', () => {fetchData((data) => {expect(data).toequal ({success: true}); })}) // The use case will not pass until done() is called manually in the callback. Test ('fetchData returns result {success: true}', (done) => {fetchData((data) => {expect(data).toequal ({success: true}); done(); })})Copy the code

Summary: The core is to pass done to the test callback argument and execute it manually.

Promise

  1. Let’s take a look at asynchronous code that executes through promises
// fetchData.js import axios from 'axios'; export const fetchData = (fn) => { return axios.get('http://www.dell-lee.com/react/api/demo1.json'); } // make an assertion in a then or catch callback, always return. Test ('fetchData returns result {success: true}', () => {return fetchData().then((resp) => {expect(resp.data).toequal ({success: true}); })}) test('fetchData returns 404', () => { return fetchData().catch((e) => { expect(e.toString().indexOf('404') > -1).toBe(true); })})}}}}}} Catch test('fetchData returns {success: true}', async () => {const resp = await fetchData(); expect(resp.data).toEqual({success: true}); }) test('fetchData returns 404', async () => {try {const resp = await fetchData(); } catch(e) { expect(e.toString().indexOf('404') > -1).toBe(true); }})???????????????? true}', () => { return expect(fetchData()).resolves.toMatchObject({ data: { success: True}})}) test('fetchData returns 404', () => {return expect(fetchData()).rejects. ToThrow (); })Copy the code

Conclusion:

  1. Using the first notation, when making an assertion in a then or catch callback, be aware of a return.
  2. Using the second method, the normal case is concise, but the exception case requires manual try… The catch.
  3. Use the third notation: the notation is more concise, but requires proficiency with the jest attributes.

Mock

The Mock timer

Mock some time-consuming code, such as timers, when you’re actually writing a test case and you don’t need to run it at the actual time.

If you want to use mock timers, you must declare jest. UseFakeTimers () in the header of the file. After this declaration, you need to call some API to check that the mock timer has been executed quickly.

  1. Use jest. RunAllTimers and jest. RunOnlyPendingTimers () quickly perform all timer or timer in the message queue.
  2. Jest. AdvanceTimersByTime () is used to indicate how long it takes to fast forward to execution. For example, if the timer is set to 3s, we can call this method.

jest.runAllTimers()

For example, the test code is as follows:

// index.js
export const getData = (callback) => {
  setTimeout(() => {
    callback({
      name: 'kobe'
    })
  },3000)
}
export default getData;
Copy the code

Thus, it is easy to write the following test case:

import {getData} from './index'; Test (' testData ', (done) => {getData((data) => {expect(data).toequal ({name: 'kobe'}); done(); })})Copy the code

The above works perfectly, but the problem is that it is executed according to the actual timer setting, which means that we have to wait 3000 milliseconds, hence the mock timer, so that we don’t have to execute according to the actual timer setting.

How about mock timers?

import {getData} from './index';
​
jest.useFakeTimers();
​
test('测试 getData', () => {
  const fn = jest.fn();
  getData(fn);
  jest.runAllTimers();
  expect(fn).toHaveBeenCalledTimes(1);
})
Copy the code

Note: You need to use jest. UseFakeTimers () in combination with jest. RunAllTimers (), which indicates that the current test case uses mock timers, and jest.

Jest. AdvanceTimersByTime () is recommended

We prefer to use this API to mock timers. Let’s take it a step further based on the above example:

Export const getData = (callback) => {setTimeout(() => {callback(); setTimeout(() => { callback(); }, 3000) },3000) } export default getData;Copy the code

If we were still using jest. RunAllTimers, we would write the following code:

import {getData} from './index'; jest.useFakeTimers(); Test (' testData ', () => {const fn = jest.fn(); getData(fn); jest.runAllTimers(); expect(fn).toHaveBeenCalledTimes(2); })Copy the code

Or it can be used in a jest. RunOnlyPendingTimers, the code is as follows:

import {getData} from './index'; jest.useFakeTimers(); Test (' testData ', () => {const fn = jest.fn(); getData(fn); jest.runOnlyPendingTimers(); expect(fn).toHaveBeenCalledTimes(1); })Copy the code

If we use jest. AdvanceTimersByTime, the code is as follows:

import {getData} from './index'; jest.useFakeTimers(); Test (' testData ', () => {const fn = jest.fn(); getData(fn); jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(1); AdvanceTimersByTime (3000) expect(fn).toHaveBeencalledTimes (2); // fast forward 3s, execute second timer callback})Copy the code

But what’s the problem with jest. AdvanceTimersByTime ()? Each call to the API fast-forwards time from the previous step. The same is true for multiple test cases. How do you resolve the interaction between test cases?

import {getData} from './index'; // That is, each test case uses a mock timer to ensure that it does not affect each other. beforeEach(() => { jest.useFakeTimers(); }) test (' test getData () = > {const fn = jest, fn (); getData(fn); jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(2); })Copy the code

A Mock function

In real development, the code being tested will probably not be a simple code with explicit input-output relationships. It will probably contain nested, callback, asynchronous code, etc. In these cases, we will use the form of mock functions, which essentially generate a function inside jEST. This function is also monitored internally by JEST to help us make assertions.

When you create a mock function using JEST, you can define the body and return value of the mock function. When the mock function is called, you can also use the internal properties provided by Jest to capture the number of times the mock function is called and its parameters.

The basic use

As a result, mock functions usually do the following:

  1. Capture the call to the function, along with the return value, the this point, and the order in which it was called
  2. It gives us freedom to set the return value of the function.
  3. You can change the internal implementation of a function

Next, let’s look at some actual code:

  1. Capture the call to the function, along with the return value, the this point, and the order in which it was called

    Test.only (' Test mock function ', () => {const func = jest.fn(); func(123); console.log(func.mock); // Assert: whether the function is called expect(func).tobecalled (); Expect (func.mock.calls.length).tobe (1); Expect (func.mock. Calls [0]).toequal ([123])})Copy the code

    Let’s look at the print result of func.mock:

As can be seen from the picture:

  1. Calls: You can see the number of times the function is called and the parameters passed each time it is called,
  2. Instances attribute: You can see the this point inside a function when it is called.
  3. InvocationCallOrder: You can see the order in which functions are called, only once.
  4. Results: You can see what the return value of each call to the function is.
  5. It gives us freedom to set the return value of the function.
Test.only (' Test mock function ', () => {const func = jest.fn(); func.mockReturnValueOnce('kobe'); func(); expect(func.mock.results[0].value).toBe('kobe'); });Copy the code

In addition, there are a few details to note:

  1. If you want to set a different return value after each function call, you can call func.mockReturnValueonce () multiple times.
  2. Func.mockreturnvalue () can be called only once if each call returns the same value
  3. You can change the internal implementation of a function
    Test.only (' Test mock function ', () => {const func = jest.fn(); func.mockImplementationOnce(() => { return 'kobe'; }) func(); expect(func.mock.results[0].value).toBe('kobe') });Copy the code

    . Note: in the same way, but can be by func mockImplementationOnce set one call the corresponding function, can according to different calls, set different function.

Practical use

Above, we simply created a mock function through jest.fn() and called it, but we didn’t actually test the code, so this section is a practical example of how to use a mock function.

Scenario: We want to write a test case for array traversal function: forEach. How do we do that?

// array.js
export const forEach = (arr, callback) =>  {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i);
  };
}
Copy the code
// array.test.js test.only(' test forEach function ', () => {const arr = [1, 2, 3, 4]; const func = jest.fn(); func.mockImplementation((item, index) => { return item + 1; }) forEach(arr, func); Expect (func.mock.calls.length).tobe (arr.length); Expect (func.mock.results[0].value).tobe (2); // Expect (func.mock.results[0].value).tobe (2); })Copy the code

Note: Mock functions are simple to use. Remember these common apis. More importantly, we need to know when to use mock functions.But when measured inside the function is more complex, may involve circulation, asynchronous logic, the scene such as a callback function, through it we need to create a mock function, and a mock function into the measured inside the function, the measured function call, the interior also call a mock function automatically, thus we can read through a mock function mock properties, To get the number of times the mock function was called, the return value, and other information to determine whether the function under test is normal.

The Mock module

Mock third-party modules

For example, when we send a request, we usually use Axios, but we don’t want to make a real request, so we can mock an Axios, and then we can set what the return value of the request is.

// index.js
import axios from "axios"
​
export const getData = () => {
  return axios.get('http://www.dell-lee.com/react/api/demo1.json').then(resp => resp.data);
}
Copy the code
// index.test.js import axios from 'axios'; import {getData} from './index'; jest.mock('axios'); Test (' test getData method, async () = > {axios. Get. MockResolvedValue ({data: {name: 'kobe' } }) const data = await getData(); expect(data).toEqual({name: 'kobe'}); })Copy the code

Mock module partial implementation – written as 1

Or a module, there may be many ways, a synchronization method, a method of asynchronous, synchronous method we generally do not need to simulation, but asynchronous method, we generally requires a corresponding simulation method, so as to ensure the test code execution, are our simulation method, does not need an actual request.

// index.js
import axios from "axios"
​
export const getData = () => {
  return axios.get('http://www.dell-lee.com/react/api/demo.json').then(resp => resp.data);
}
​
export const sum = (a, b) => {
  return a + b;
}
Copy the code

In this module, there are two methods. Since getData needs to make asynchronous requests and we don’t want to send real mock requests in the test code, we need to create a corresponding mock function, while sum is a simple synchronous function, which can be directly introduced to the test.

Note: If we want to mock a module, we can create a __mocks__ folder in its sibling and create a file with the same name in the change folder.

// __mocks__/index.js
export const getData = () => {
  return new Promise((resolve, reject) => {
    resolve({
      name: 'james'
    })
  })
}
Copy the code

Next, we need to write the test case. Since we will mock the index.js module, we need to manually call jest. Mock (./index) to indicate that the test case will execute the mock function.

jest.mock('./index') import {getData, sum} from "./index"; Test (' test getData method ', async () => {const res = await getData(); Expect (res).toequal ({name: 'James'})}) // The use case fails because the mock module does not have sum. Test (test method of sum, async () = > {expect (sum (1, 2)). The place (3); })Copy the code

In the above code, we used mock modules, but when we tested the sum function, we wanted to use real modules, so the second test case in the above code failed, so we need to modify it as follows:

jest.mock('./index') import { getData} from "./index"; Const {sum} = jest. RequireActual ('./index.js') // test(' test method ', async () => {const res = await getData(); Expect (res).toequal ({name: 'James'})}) test(' sum method ', async () => {expect(sum(1, 2)).tobe (3); })Copy the code

Mock module partial implementation – notation 2

In script 1, we create a __mocks__ folder in the same directory as the module under test, and create a mock module with the same name in that folder. Of course, we can also mock some of the methods in the mock module directly in the test file.

jest.mock('./index', () => { return { getData: jest.fn(() => { return new Promise((resolve, reject) => { resolve({ name: 'james' }) }) }) } }) import { getData} from "./index"; Const {sum} = jest. RequireActual ('./index.js') test(' test getData method ', async () => {const res = await getData(); Expect (res).toequal ({name: 'James'})}) test(' sum method ', async () => {expect(sum(1, 2)).tobe (3); })Copy the code

The way I wrote it, I could actually optimize it a little bit

Mock ('./index', () => {// Mock ('./index', () => {// const originalModule = jest.requireActual('./index'); return { ... originalModule, getData: jest.fn(() => { return new Promise((resolve, reject) => { resolve({ name: 'james' }) }) }) } }) import { getData} from "./index"; Test (' test getData method ', async () => {const res = await getData(); Expect (res).toequal ({name: 'James'})}) test(' sum method ', async () => {expect(sum(1, 2)).tobe (3); })Copy the code

The Mock class

For example, we have an Util class like this:

// util.js
class Util {
  a () {}
  b() {}
}
export default Util
Copy the code

Of course, it also has a corresponding test file.

// util.test.js import Util from './util' let util = null; beforeAll(() => { util = new Util(); }) test (' test Util a method, () = > {expect (Util. A ()). ToBeUndefined (); }) test (' test Util method b, () = > {expect (Util. B ()). ToBeUndefined (); })Copy the code

Obviously, if we wanted to test a method in a class, it would be easy, but the above code is not the point here. The scenario is: how do we test methods in Util when they are referenced in other files?

Such as:

// demo.js
import Util from './util'
​
export const demoFunction = () => {
  const util = new Util();
  util.a();
  util.b();
}
Copy the code

We want to test demoFunction, so how do we write test cases? Before we write, we need to understand: Test the demoFunction method. Although it internally references the methods associated with Util, we do not need to test the methods in Util because we have already tested the methods in Util. Test.js. Here we just need to check whether the method in Util is called.

Therefore, we can Mock a class to ensure that the methods in the Mock class are called. The implementation is as follows:

Jest. Mock ('./util ') import util from './util' import {demoFunction} from './demo' test(' test demoFunction', 'test demoFunction') () => { demoFunction(); expect(Util).toHaveBeenCalled(); expect(Util.mock.instances[0].a).toHaveBeenCalled(); expect(Util.mock.instances[0].b).toHaveBeenCalled(); })Copy the code

When we declare jest. Mock (‘./util.js’), jest automatically turns the imported util into a mock function and all its internal methods into mock functions, so we can use the features of the mock function directly to determine whether the function is called or not.

Mock: Util. Mock

Note: Mock third party modules like Axios essentially mock a class and then call methods inside the mock class. See the code again:

jest.mock('axios') import axios from 'axios'; The test (' test request methods, () = > {the console. The log (axios. Get. MockReturnValue ({name: "kobe"})); })Copy the code

At the same time, you can manually create a __mock__ folder and a file of the same name to mock custom methods in Util.

So there are really four ways to mock a class:

  1. Mock automatically via jest. Mock (‘./utils.js’)
  2. Manually simulate by creating a __mocks__ file with the same name
  3. Mock (‘,/util.js’, () => {}) is the second argument to return the mock function manually.
  4. Of course, in one of our test cases, we used mockImplementation alone to simulate the implementation of a function.

Dom test

Jest is integrated with Jsdom dependencies, so we can also manipulate dom directly using Jsdom related apis.

Let’s try it out:

function renderHtml() {
  const div = document.createElement("div");
  div.innerHTML = "<h1>Hello World</h1>";
  document.body.appendChild(div);
}
​
test("DOM Test:", () => {
  renderHtml();
  expect(document.querySelector("h1").innerHTML).toBe("Hello World");
});
Copy the code

A snapshot of the test

As the name suggests, when our components have been completed or don’t want to change, we can write good components or dom structure to generate a snapshot, is actually an HTML string, if inadvertently modify the dom structure, after the snapshot test at this moment, we would be prompted to generate a snapshot of the two, thus further to judge which one shall prevail.

Let’s try it out:

function renderHtml() { const div = document.createElement("div"); div.innerHTML = "<h1>Hello World1</h1>"; document.body.appendChild(div); } test("DOM Test:", () => { renderHtml(); expect(document.querySelector("h1").innerHTML).toBe("Hello World"); }); test("Snapshot Test:", () => { expect(document.body).toMatchSnapshot(); // Snapshot files are generated for the first time. });Copy the code

After that, if we want to update the snapshot file, which we can, we can just run the following command

jest --updateSnapshot
Copy the code

Test coverage

Test coverage is the percentage of the total code executed, such as lines, branches, functions, and statements. Therefore, coverage is mainly divided into the following types:

  1. Line coverage Lines
  2. Branch Coverage
  3. Function coverage Funcs
  4. Statement coverage Stmts

Practical cases

For example, unit test a Stack class.

The code under test

// stack.js export default class Stack { constructor () { this.stack = []; } push (... element) { this.stack.push(... element); } pop () { if (this.isEmpty()) { return undefined; } return this.stack.pop(); } peek () { if (this.isEmpty()) { return undefined; } return this.stack[this.stack.length - 1] } isEmpty () { return this.stack.length === 0; } clear () { this.stack = []; } size () { return this.stack.length; }}Copy the code

bad case

As a result, it is easy to write unit tests like this:

// stack.test.js
test('test push', () => {
  let stack = new Stack();
  stack.push(1);
  stack.push(2);
  expect(stack.size()).toBe(2);
});
​
test('test pop', () => {
  let stack = new Stack();
  expect(stack.pop()).toBeUndefined();
​
  stack.push(1);
  stack.push(2);
  expect(stack.pop()).toBe(2);
  expect(stack.pop()).toBe(1);
  expect(stack.pop()).toBeUndefined();
});
​
test('test peek', () => {
  let stack = new Stack();
  expect(stack.peek()).toBeUndefined();
  stack.push(1);
  stack.push(2);
  expect(stack.peek()).toBe(2);
});
​
test('test isEmpty', () => {
  let stack = new Stack();
  expect(stack.isEmpty()).toBe(true);
  stack.push(1);
  expect(stack.isEmpty()).toBe(false);
});
​
test('test clear', () => {
  let stack = new Stack();
  stack.push(1);
  expect(stack.size()).toBe(1);
  stack.clear();
  expect(stack.size()).toBe(0);
});
​
test('test size', () => {
  let stack = new Stack();
  stack.push(1);
  expect(stack.size()).toBe(1);
});
Copy the code

However, the above code looks as if it has written test cases for every method with 100% test coverage, but there are a number of problems with the above test cases:

  1. Use case structure to classify, such as the same method test cases, we can use Describle surrounded, so the overall structure is clearer
  2. To reuse properly, the above code creates stack instances in each test case, which is not necessary. You can reuse stack instances and use lifecycle methods wisely.
  3. Customizing common parts as needed means that if there is some reusable logic in multiple test cases, we can abstract it out.
  4. Intent-oriented, not object-oriented.

good case

All test cases are intent-oriented, not object-oriented. Our test code above made this mistake. We simply add one test case for each method in the Stack, but many methods are linked, so it’s not necessary to design test cases from the quantitative dimension of methods, but from the intent, or from the actual usage perspective.

In addition, each method will have many situations with different parameters and return values, so at this time, we should design corresponding test cases for each possible situation.

// stack.test.js describe('init stack', () => { let stack; beforeEach(() => { stack = new Stack(); }); test('default length', () => { expect(stack.items.length).toBe(0); }); }); describe('push method', () => { let stack; beforeEach(() => { stack = new Stack(); }); Test ('when push single, size increase 1', () => {stack.push(1); expect(stack.items.length).toBe(1); }); test('when push multiple, size increase multiple', () => { stack.push(1, 2, 3, 4); expect(stack.items.length).toBe(4); }); test('when push, get correct last item', () => { stack.push(1); expect(stack.items).toEqual([1]); }); }); describe('pop method', () => { let stack; beforeEach(() => { stack = new Stack(); stack.push(1, 2, 3); }); test('when pop, size decrease 1', () => { expect(stack.items.length).toBe(3); stack.pop(); expect(stack.items.length).toBe(2); }); test('when pop, return last item', () => { expect(stack.pop()).toBe(3); }); test('when pop and stack is empty, return undefined', () => { stack.pop(); stack.pop(); stack.pop(); expect(stack.pop()).toBeUndefined(); }); }); describe('peek method', () => { let stack; beforeEach(() => { stack = new Stack(); stack.push(1, 2, 3); }); test('when stack is not empty, return last item', () => { expect(stack.peek()).toBe(3); }); test('when stack is empty, return undefined', () => { stack.clear(); expect(stack.peek()).toBeUndefined(); }); }); describe('clear method', () => { let stack; beforeEach(() => { stack = new Stack(); stack.push(1, 2, 3); }); test('when clear, size is 0', () => { stack.clear(); expect(stack.items.length).toBe(0); }); }); describe('size method', () => { let stack; beforeEach(() => { stack = new Stack(); stack.push(1, 2, 3); }); test('return correct size', () => { expect(stack.size()).toBe(3); }); }); describe('isEmpty method', () => { let stack; beforeEach(() => { stack = new Stack(); }); test('when stack is empty, return true', () => { expect(stack.isEmpty()).toBe(true); }); test('when stack is empty, return false', () => { stack.push(1, 2, 3); expect(stack.isEmpty()).toBe(false); }); });Copy the code

Description:

  1. Instead of just testing the Stack method, we also did unit tests for its actual use, such as Stack initialization operations.
  2. Push, POP and other methods, we have done corresponding test cases for parameters and return values under different circumstances.

Case summary

Through this simple case, we can clearly feel how to better write test code, usually pay attention to the following points:

  1. To make good use of describe, organize test cases of a common type so that the overall structure is clearer.
  2. To make good use of lifecycle hook functions, we can usually do some reusable operations in these hook functions, such as data initialization, destruction, etc.
  3. If there is common logic between test cases, we can also abstract it into a separate method.
  4. The writing of test cases should be based on their actual use, rather than purely object-oriented writing test cases.

conclusion

Write unit tests, and we usually write business code, in fact, there is no essential difference between the knowledge points, used by including Jest to provide these apis, actually by in front of us, look at the website, we all can grasp, basic introduction to the individual feels the most trouble thing is the change of thinking mode, especially the Mock this, We need to know when to Mock. After reading this section, you can write your own code to experience it. We will have a basic understanding and application of unit testing.

Writing is not easy, if you feel good, please like and follow 😊😊😊