🏂 write in front
Regarding the front-end unit test, I actually paid attention to it two years ago, but at that time I simply knew the assertion, thinking that it was not too difficult, and it was not used in the project, and then I assumed that I would do it for granted. .
Today, two years later, the department needs to add unit tests to previous projects. When it was really time to start, but I was confused 😂
What I thought I thought I had pitted myself, and I found that I didn't know anything about front-end unit testing. Then I read a lot of documents, found on dva
unit test documentation is relatively small, so after some practice with, I brushed a few articles, I hope to think use Jest were React + Dva + Antd unit testing you Can help. The content of the article strives to be in-depth and simple to understand~
Because the content is all contained in one article, it will be too long. The plan is divided into two articles. This article is the first one. It mainly introduces how to quickly get started withjest
and the functions commonly used in actual combat andapi
🏈 The background of front-end automated testing
Before introducing jest
, I think it is necessary to briefly explain some basic information about front-end unit testing.
- Why test?
Today in 2021, it is not difficult for us to
web
Because there are enough excellent front-end frameworks (such asReact
,Vue
); and some easy-to-use and powerfulUI
libraries (such asAnt Design
,Element UI
) to escort us, greatly shortening the cycle of application construction. However, the rapid iteration process has produced a lot of problems: low code quality (poor readability, low maintainability, low scalability), frequent product demand changes (uncontrollable range of code changes), etc.Therefore, the concept of unit testing came into being in the front-end field. By writing unit tests, you can ensure that you get the expected results and improve the readability of the code. If the dependent components are modified, the affected components can also find errors in the test in time.
What are the test types?
There are generally the following four types:
- unit test
- function test
- Integration Testing
- Smoke test
What about common development models?
TDD
: Test Driven DevelopmentBDD
: Behavior Driven Test
🎮 Technical solution
For the project itself, React + Dva + Antd
is used, and the unit test we use is the combination of Jest + Enzyme
Jest
About Jest
, we refer to it Jest official website , it is Facebook
open source front end of a testing framework, mainly for React
and React Native
unit testing, it has been integrated in create-react-app
in. Jest
features:
- Zero configuration
- Snapshot
- isolation
- Excellent api
- Fast and safe
- Code coverage
- Simulate easily
- Excellent error message
Enzyme
Enzyme
is the Airbnb
open source React
. It provides a set of simple and powerful API
and built-in Cheerio
. At the same time, it realizes the jQuery
style method for DOM
processing. The development experience is very friendly. React
popular in the open source community and has also been officially recommended by 06076ab16bfd9d.
📌 Jest
In this article, we focus on introducing Jest
, which is also the foundation of React unit test.
Environment setup
installation
Install Jest
, Enzyme
. If React
version is 15
or 16
, you need to install the corresponding enzyme-adapter-react-15
and enzyme-adapter-react-16
and configuration.
/**
* setup
*
*/
import Enzyme from "enzyme"
import Adapter from "enzyme-adapter-react-16"
Enzyme.configure({ adapter: new Adapter() })
jest.config.js
You can run npx jest --init
to generate the configuration file jest.config.js
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/en/configuration.html
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// An array of directory names to be searched recursively up from the requiring module's location
moduleDirectories: ["node_modules", "src"],
// An array of file extensions your modules use
moduleFileExtensions: ["js", "json", "jsx", "ts", "tsx"],
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: [
"./node_modules/jest-enzyme/lib/index.js",
"<rootDir>/src/utils/testSetup.js",
],
// The test environment that will be used for testing
testEnvironment: "jest-environment-jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// The glob patterns Jest uses to detect test files
testMatch: ["**/?(*.)+(spec|test).[tj]s?(x)"],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: ["/node_modules/", "\\.pnp\\.[^\\/]+$"],
}
Here is just a list of commonly used configuration items:
automock
: Tell Jest that all modules are automatically imported from the mock.clearMocks
: Automatically clean up mock calls and instance instances before each testcollectCoverage
: Whether to collect coverage information during testingcollectCoverageFrom
: Coverage file detected when generating test coverage reportcoverageDirectory
: The directory where Jest outputs the coverage information filecoveragePathIgnorePatterns
: List of files excluded from coveragecoverageReporters
: List a list of reporter names, and Jest will use them to generate coverage reportscoverageThreshold
: The threshold at which the test can passmoduleDirectories
: Module search pathmoduleFileExtensions
: Represents the file name that supports loadingtestPathIgnorePatterns
: Use regular to match files that do not need to be testedsetupFilesAfterEnv
: configuration file, before running the test case code, Jest will first run the configuration file here to initialize the specified test environmenttestMatch
: Define the file to be testedtransformIgnorePatterns
: Set which files do not need to be translatedtransform
: Set the codes in which files need to be converted into codes that Jest can recognize by the corresponding translator. Jest can recognize JS codes by default. Other languages, such as Typescript, CSS, etc., need to be translated.
Matcher
toBe(value)
: Use Object.is to compare, if you compare floating-point numbers, use toBeCloseTonot
: InverttoEqual(value)
: Used for deep comparison of objectstoContain(item)
: Used to judge whether the item is in an array, or it can be used to judge a stringtoBeNull(value)
: only match nulltoBeUndefined(value)
: only match undefinedtoBeDefined(value)
: Contrary to toBeUndefinedtoBeTruthy(value)
: match any sentence with a true valuetoBeFalsy(value)
: match any statement that is falsetoBeGreaterThan(number)
: greater thantoBeGreaterThanOrEqual(number)
: greater than or equal totoBeLessThan(number)
: less thantoBeLessThanOrEqual(number)
: less than or equal totoBeInstanceOf(class)
: Determine whether it is an instance of classresolves
: Used to retrieve the value of the package when the promise is fulfilled, support chain callrejects
: Used to retrieve the value of the package when the promise is rejected, and supports chained callstoHaveBeenCalled()
: used to determine whether the mock function has been calledtoHaveBeenCalledTimes(number)
: used to determine the number of times the mock function is calledassertions(number)
: Verify that there are number assertions called in a test case
Use of command line tools
Add the following script
project package.json
file:
"scripts": {
"start": "node bin/server.js",
"dev": "node bin/server.js",
"build": "node bin/build.js",
"publish": "node bin/publish.js",
++ "test": "jest --watchAll",
},
npm run test
at this time:
We found the following patterns:
f
: Only test cases that have not passed before will be testedo
: Only the associated and changed files will be tested (git is required) (jest --watch can directly enter this mode)p
: Test case where the test file name contains the entered namet
: Test case whose name contains the entered namea
: Run all test cases
During the test, you can switch to the appropriate mode.
Hook function
Similar to the life cycle of react or vue, there are four types:
beforeAll()
: The method to be executed before all test cases are executedafterAll()
: The method to be executed after all test cases are runbeforeEach()
: The method that needs to be executed before each test case is executedafterEach()
: The method to be executed after each test case is executed
Here, I use a basic demo
in the project to demonstrate the specific use:
Counter.js
export default class Counter {
constructor() {
this.number = 0
}
addOne() {
this.number += 1
}
minusOne() {
this.number -= 1
}
}
Counter.test.js
import Counter from './Counter'
const counter = new Counter()
test('测试 Counter 中的 addOne 方法', () => {
counter.addOne()
expect(counter.number).toBe(1)
})
test('测试 Counter 中的 minusOne 方法', () => {
counter.minusOne()
expect(counter.number).toBe(0)
})
Run npm run test
:
By adding 1 to the first test case, number
is 1. When the second case is subtracted by 1, the result should be 0. However, the interference between the two use cases is not good, which can be Jest
by the hook function of 06076ab16c04ed. Modify the test case:
import Counter from "../../../src/utils/Counter";
let counter = null
beforeAll(() => {
console.log('BeforeAll')
})
beforeEach(() => {
console.log('BeforeEach')
counter = new Counter()
})
afterEach(() => {
console.log('AfterEach')
})
afterAll(() => {
console.log('AfterAll')
})
test('测试 Counter 中的 addOne 方法', () => {
counter.addOne()
expect(counter.number).toBe(1)
})
test('测试 Counter 中的 minusOne 方法', () => {
counter.minusOne()
expect(counter.number).toBe(-1)
})
Run npm run test
:
You can clearly see the execution order of the corresponding hooks:
beforeAll> (beforeEach> afterEach) (a single use case will be executed in sequence)> afterAll
In addition to the above basic knowledge, there are actually asynchronous code testing, Mock, Snapshot snapshot testing, etc., which we will explain in turn in the following React unit test examples.
Asynchronous code testing
As we all know, JS
is full of asynchronous code.
Under normal circumstances, the test code is executed synchronously, but when the code we want to test is asynchronous, there will be a problem: test case
actually ended, but our asynchronous code has not been executed yet, resulting in the asynchronous code not being tested To.
then what should we do?
For the current test code, the asynchronous code does not know when to execute it, so the solution is simple. When there is asynchronous code, the test code does not end immediately after running the synchronous code, but waits for the end notification. When the asynchronous code is executed, tell jest
: "Okay, the asynchronous code is executed, you can end the task" .
jest
provides three solutions to test asynchronous code, let's take a look at them separately.
done keyword
When an asynchronous callback function appears in test
function, we can pass in a done
test
function, which is a function type parameter. If the test
function is passed in done
, jest
will wait until done
is called before ending the current test case
. If done
not called, the test
will automatically fail the test.
import { fetchData } from './fetchData'
test('fetchData 返回结果为 { success: true }', done => {
fetchData(data => {
expect(data).toEqual({
success: true
})
done()
})
})
The above code, we have to test
function was introduced to done
parameters, in fetchData
call a callback function done
. In this way, the test code executed asynchronously in the callback of fetchData
But here we think about a scenario: if you use done
to test the callback function (including the timer scenario, such as setTimeout
), because the timer we set a certain delay (such as 3s) and execute it, we will find that the test passes after waiting for 3s. . So if setTimeout
set to a few hundred seconds, should we also test after a few hundred seconds Jest
Obviously, this greatly reduces the efficiency of the test! !
jest
jest.useFakeTimers()
such as 06076ab16c0687, jest.runAllTimers()
and toHaveBeenCalledTimes
, jest.advanceTimersByTime
etc. api
to deal with this kind of scene.
I will not give an example here in detail, students who have this need can refer to Timer Mocks
Return Promise
⚠️ WhenPromise
, be sure to add areturn
before the assertion, otherwise the test function will endPromise
You can use.promises/.rejects
to get the returned value, or use thethen/catch
method for judgment.
If the code used in Promise
, it can return Promise
to handle asynchronous code, jest
will wait for the promise
state into resolve
will end, if promise
is reject
, and does not pass the test.
// 假设 user.getUserById(参数id) 返回一个promise
it('测试promise成功的情况', () => {
expect.assertions(1);
return user.getUserById(4).then((data) => {
expect(data).toEqual('Cosen');
});
});
it('测试promise错误的情况', () => {
expect.assertions(1);
return user.getUserById(2).catch((e) => {
expect(e).toEqual({
error: 'id为2的用户不存在',
});
});
});
Note that the second test case above can be used to test the situation where promise
returns reject
Here we use .catch
to capture the promise
returned by reject
. When promise
returns reject
expect
statement will be executed. expect.assertions(1)
here is used to ensure that a expect
is executed in the test case.
For Promise
case, jest
also provides a pair of matching symbols resolves/rejects
, in fact, just syntactic sugar written above. The above code can be rewritten as:
// 使用'.resolves'来测试promise成功时返回的值
it('使用'.resolves'来测试promise成功的情况', () => {
return expect(user.getUserById(4)).resolves.toEqual('Cosen');
});
// 使用'.rejects'来测试promise失败时返回的值
it('使用'.rejects'来测试promise失败的情况', () => {
expect.assertions(1);
return expect(user.getUserById(2)).rejects.toEqual({
error: 'id为2的用户不存在',
});
});
async/await
We know that async/await
is Promise
the syntactic sugar of 06076ab16c07e3, which can be used to write asynchronous code more elegantly. This kind of syntax is also supported in jest
Let's rewrite the above code:
// 使用async/await来测试resolve
it('async/await来测试resolve', async () => {
expect.assertions(1);
const data = await user.getUserById(4);
return expect(data).toEqual('Cosen');
});
// 使用async/await来测试reject
it('async/await来测试reject', async () => {
expect.assertions(1);
try {
await user.getUserById(2);
} catch (e) {
expect(e).toEqual({
error: 'id为2的用户不存在',
});
}
});
⚠️ Useasync
withoutreturn
, and usetry/catch
to catch exceptions.
Mock
Before introducing jest
in mock
, let's first think about a question: Why use the mock
function?
In a project, a method of a module often calls a method of another module. In unit testing, we may not need to care about the execution process and results of the internally called method, just want to know whether it is called correctly, and even specify the return value of the function. At this time, mock
is of great significance.
jest
are mainly three mock
related to api
jest.fn()
, jest.mock()
, jest.spyOn()
. Using them to create the mock
function can help us better test some logically complex codes in the project. In our test, we mainly used the following three features provided by mock
- Capturing function calls
- Set function return value
- Change the internal implementation of the function
Below, I will introduce these three methods and their application in actual testing.
jest.fn()
jest.fn()
is the easiest way to create the mock
function. If the internal implementation of the function is not defined, jest.fn()
will return undefined
as the return value.
// functions.test.js
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
let res = mockFn('厦门','青岛','三亚');
// 断言mockFn的执行后返回undefined
expect(res).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith('厦门','青岛','三亚');
})
jest.fn()
created mock
function return values may also be provided, define internal implementation or
return Promise objects.
// functions.test.js
test('测试jest.fn()返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 断言mockFn执行后返回值为default
expect(mockFn()).toBe('default');
})
test('测试jest.fn()内部实现', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 + num2;
})
// 断言mockFn执行后返回20
expect(mockFn(10, 10)).toBe(20);
})
test('测试jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('default');
let res = await mockFn();
// 断言mockFn通过await关键字执行后返回值为default
expect(res).toBe('default');
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
jest.mock()
Generally, in real projects, when testing asynchronous functions, the ajax
request will not be sent to request this interface. Why?
For example, there are 1w
interfaces to be tested, and each interface requires 3s
to return. To test all interfaces, 30000s
required, so the time for this automated test is too slow
As the front-end, we only need to confirm that the asynchronous request is sent successfully. As for the content returned by the back-end interface, we will not test it. This is what the back-end automated test needs to do.
Here is an 06076ab16c09ed requested by
demo
:
// user.js
import axios from 'axios'
export const getUserList = () => {
return axios.get('/users').then(res => res.data)
}
Corresponding test file user.test.js
:
import { getUserList } from '@/services/user.js'
import axios from 'axios'
// 👇👇
jest.mock('axios')
// 👆👆
test.only('测试 getUserList', async () => {
axios.get.mockResolvedValue({ data: ['Cosen','森林','柯森'] })
await getUserList().then(data => {
expect(data).toBe(['Cosen','森林','柯森'])
})
})
jest.mock('axios')
the top of the test case, and we let jest
axios
so that it would not request real data. Then axios.get
is called, it will not actually request this interface, but will use our {data: ['Cosen','Forest','Cosen']} to simulate the result of the successful request.
Of course, it takes time to simulate asynchronous requests. If there are many requests, it will take a long time. At this time, you canmock
data in the local 06076ab16c0a83 and create a new folder__mocks__
This method does not need to simulateaxios
, but a direct local simulation method, which is also a more commonly used method, which will not be explained here.
jest.spyOn()
jest.spyOn()
method also creates a mock
function, but the mock
function can not only capture the function call, but also execute the function spy
In fact, jest.spyOn()
is jest.fn()
syntactic sugar, and it creates a being spy
function with the same internal code mock function.
Snapshot test
The so-called snapshot
is also a snapshot. Usually involving automated testing of UI, the idea is to take a snapshot of the standard state at a certain moment.
describe("xxx页面", () => {
// beforeEach(() => {
// jest.resetAllMocks()
// })
// 使用 snapshot 进行 UI 测试
it("页面应能正常渲染", () => {
const wrapper = wrappedShallow()
expect(wrapper).toMatchSnapshot()
})
})
When using toMatchSnapshot
, Jest
will render the component and create its snapshot file. This snapshot file contains the entire structure of the rendered component and should be submitted to the code base along with the test file itself. When we run the snapshot test again, Jest
will compare the new snapshot with the old snapshot. If the two are inconsistent, the test will fail, which helps us ensure that the user interface does not change unexpectedly.
🎯 Summary
Here, some basic background on the front of unit testing and Jest
based api
will introduce over the next article, I will combine a project React component to explain how to do
component unit testing.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。