🏂 comes first
In terms of front-end unit testing, I’ve been paying attention to it since two years ago, but at that time I simply knew assertions, thought about things that weren’t too difficult, didn’t use them in my projects, and took it for granted that I could do it.
Now, two years later, the department is adding unit tests to previous projects. Really arrived at the beginning of the time, but meng 😂
What I thought I thought I screwed myself up by discovering that I knew nothing about front-end unit testing. Then I went through a lot of documents, found that dVA-BASED unit test documents are very few, so after some practice, I sorted out a few articles, hoping to help you who want to use Jest for React + DVA + Antd unit test. The content of the article strives to be simple and easy to understand
Since it would be too long to contain all the contents in one article, we plan to divide it into two parts. This is the first part, which mainly introduces how to quickly get started with JEST and the functions and APIS commonly used in the actual practice
🏈 Background to front-end automation test generation
Before getting started with JEST, I thought it would be worth briefly explaining some basic information about front-end unit testing.
-
Why test?
In 2021, it’s not that hard to build a complex Web application. There are plenty of good front-end frameworks (React, Vue); Easy to use and powerful UI libraries such as Ant Design and Element UI have helped us greatly reduce the application build cycle. However, in the process of rapid iteration, a large number of problems were generated: low code quality (poor readability, low maintainability, low scalability), frequent changes in product requirements (the influence range of code changes is uncontrollable), etc.
Therefore, the concept of unit testing emerged in the front-end domain. By writing unit tests, we can ensure the expected results, improve the readability of code, and if the dependent components change, the affected components can detect errors in the test in time.
-
What are the types of tests?
Generally, there are four kinds of common:
- Unit testing
- A functional test
- Integration testing
- Smoke test
-
What are common development patterns?
TDD
: Test-driven developmentBDD
: Behavior driven testing
🎮 Technical Solution
React + Dva + Antd technology stack is used for the project itself, and Jest + Enzyme method is used for unit test.
Jest
Jest is Facebook’s open source front-end testing framework for React and React Native unit testing, which is integrated into create-React-app. Jest features:
- Zero configuration
- The snapshot
- isolation
- Excellent API
- Fast and safe
- Code coverage
- Easy to simulate
- Good error message
Enzyme
Enzyme is Airbnb’s open source React test tool library, which provides a simple and powerful API with built-in Cheerio. Meanwhile, DOM processing is implemented in a jquery-style way, making the development experience very friendly. It has gained popularity in the open source community and was officially featured by React.
📌 Jest
This article focuses on Jest, which is the foundation for our entire React unit test.
Environment set up
The installation
Install Jest, Enzyme. If the React version is 15 or 16, install the corresponding enzyme- Adapter-react -15 and enzyme- Adapter-react -16 and configure them.
/** * setup * */
import Enzyme from "enzyme"
import Adapter from "enzyme-adapter-react-16"
Enzyme.configure({ adapter: new Adapter() })
Copy the code
jest.config.js
You can run NPX jest –init to generate the configuration file jest.config.js in the root directory
/*
* 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\\.[^\\/]+$"],
}
Copy the code
Common configuration items are listed here:
automock
: tells Jest that all modules are automatically imported from the mock.clearMocks
: Automatically cleans up mock calls and instances before each testcollectCoverage
: Whether to collect coverage information at test timecollectCoverageFrom
: The coverage file tested when generating the test coverage reportcoverageDirectory
: Jest directory for output overwrite information filescoveragePathIgnorePatterns
: Exclude coverage’s file listcoverageReporters
: Lists lists of reporter names that Jest uses to generate coverage reportscoverageThreshold
: Indicates the threshold for the test to passmoduleDirectories
: Module search pathmoduleFileExtensions
: indicates the file name that can be loadedtestPathIgnorePatterns
: Uses re to match files that are not being testedsetupFilesAfterEnv
: configuration file, which Jest runs to initialize the specified test environment before running the test case codetestMatch
: Defines the file to be testedtransformIgnorePatterns
: Sets which files do not need to be translatedtransform
Jest recognizes JAVASCRIPT code by default. Other languages, such as Typescript, CSS, etc., need to be translated.
matcher
toBe(value)
: Use object. is for comparison, or toBeCloseTo for floating-point comparisonsnot
: take thetoEqual(value)
: used for deep comparisons of objectstoContain(item)
: is used to check whether an item is in an array. It can also be used to check stringstoBeNull(value)
: Matches only nulltoBeUndefined(value)
: Matches only undefinedtoBeDefined(value)
: The opposite of toBeUndefinedtoBeTruthy(value)
: matches any statement as truetoBeFalsy(value)
: Matches any value whose statement is falsetoBeGreaterThan(number)
: more thantoBeGreaterThanOrEqual(number)
: Greater than or equal totoBeLessThan(number)
: less thantoBeLessThanOrEqual(number)
: Less than or equal totoBeInstanceOf(class)
: Determines whether it is an instance of classresolves
: Is used to take out the value of the package when the promise is fulfilled, which supports chain callrejects
: fetches the value of the package rejected when promise is rejectedtoHaveBeenCalled()
: used to determine whether the mock function has been calledtoHaveBeenCalledTimes(number)
: determines how many times the mock function is calledassertions(number)
Verify that there are a number of assertions called in a test case
Use of command line tools
Add the following script to the 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",
},
Copy the code
At this time to runnpm run test
:
We found the following patterns:
f
: Tests only test cases that have not previously passedo
: tests only associated and changed files (using git) (jest –watch can enter this mode directly)p
: Test file name contains the test case of the entered namet
: Test case name Test case that contains the entered namea
: Runs all test cases
During testing, you can switch between the appropriate modes.
Hook function
The React or Vue life cycle has four types:
beforeAll()
: Methods executed before all test cases are executedafterAll()
: Method to execute after all test cases have runbeforeEach()
: Methods that need to be executed before each test case is executedafterEach()
: Method to execute after each test case is executed
Here, I use a basic demo from the project to show how to use it:
Counter.js
export default class Counter { constructor() { this.number = 0 } addOne() { this.number += 1 } minusOne() { this.number - = 1}}Copy the code
Counter.test.js
import Counter from './Counter'
const counter = new Counter()
test('Test addOne method in Counter'.() = > {
counter.addOne()
expect(counter.number).toBe(1)
})
test('Test the minusOne method in Counter'.() = > {
counter.minusOne()
expect(counter.number).toBe(0)})Copy the code
runnpm run test
:
By incrementing the first test case by 1, the value of number is 1. When the second test case is subtracted by 1, the result should be 0. However, such two use cases do not interfere with each other, which can be solved by Jest’s hook function. Modify test cases:
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('Test addOne method in Counter'.() = > {
counter.addOne()
expect(counter.number).toBe(1)
})
test('Test the minusOne method in Counter'.() = > {
counter.minusOne()
expect(counter.number).toBe(-1)})Copy the code
runnpm run test
:
You can clearly see the sequence of hooks:
BeforeAll > (beforeEach > afterEach)(individual use cases are executed sequentially) > afterAll
In addition to these basics, there are also asynchronous code testing, mocks, and Snapshot testing, which we’ll cover in the unit test example on React.
Testing asynchronous code
As we all know, JS is full of asynchronous code.
Normally the test code is executed synchronously, but when the code we are testing is asynchronous, there is a problem: the test case has actually ended, but our asynchronous code has not yet been executed, resulting in the asynchronous code not being tested.
So what to do?
It is not known to the currently tested code when the asynchronous code executes it, so the solution is simple. When there is asynchronous code, the test code does not finish immediately after the synchronous code runs. Instead, the test code waits for the completion notification, and then tells jest when the asynchronous code is finished: “Ok, the asynchronous code is finished, you can finish the task.”
Jest provides three ways to test asynchronous code, and let’s take a look at each.
Done a keyword
When an asynchronous callback occurs in our test function, we can pass a done argument to the test function, which is a function type argument. If the test function passes done, jest will wait until done is called before terminating the current test case. If done is not called, the test will automatically fail.
import { fetchData } from './fetchData'
test('fetchData 返回结果为 { success: true }'.done= > {
fetchData(data= > {
expect(data).toEqual({
success: true
})
done()
})
})
Copy the code
In the above code, we pass the done argument to the test function and call done in the fetchData callback. In this way, the test code that is executed asynchronously in fetchData’s callback can be executed.
But here we consider a scenario: if we use done to test the callback function (including timer scenarios, such as setTimeout), we set a certain delay (such as 3s) after the timer is executed, and wait for 3s to find that the test passes. If setTimeout is set to a few hundred seconds, should we wait a few hundred seconds before testing Jest?
Obviously, this is very inefficient for testing!!
Apis such as jest.usefaketimers (), jest.RunallTimers () and toHaveBeenCalledTimes, jest.AdvanceTimersbyTime are provided in Jest to handle this scenario.
I’m not going to give you an example here, but if you need one, you can refer to Timer Mocks
Return to the Promise
⚠️ When testing a Promise, be sure to precede the assertion with a return, or the test function will end without waiting for the Promise’s return. You can use.promises/.rejects to obtain the returned value, or use the then/catch method to judge.
If a Promise is used in the code, the asynchronous code can be processed by returning a Promise, and jEST will wait until the state of the Promise changes to resolve. If the Promise is rejected, the test case fails.
// Suppose user.getUserById (parameter id) returns a Promise it(' Testing the promise success ', () => {expect. Assertions (1); return user.getUserById(4).then((data) => { expect(data).toEqual('Cosen'); }); }); It (' Test promise error ', () => {expect. Assertions (1); Return user.getUserById(2). Catch ((e) => {expect(e).toequal ({error: 'there is no user with id 2 ',}); }); });Copy the code
Note that the second test case above can be used to test the case where a PROMISE returns REJECT. A. Catch is used to catch the reject returned by the promise, and when the promise returns reject, the Expect statement is executed. Here, expect. Assertions (1) are used to ensure that one of expect is executed in this test case.
In the case of Promise, Jest also offers a pair of high similarity /rejects, which is really just a grammatical sugar from the above notation. The above code can be rewritten as:
// Use '.convergency 'to test the value returned when the promise succeeds
it('use'.resolves'To test the promise's success'.() = > {
return expect(user.getUserById(4)).resolves.toEqual('Cosen');
});
// Use '.rejects' to test the value returned when a promise fails
it('use'.rejects'To test if a promise fails.'.() = > {
expect.assertions(1);
return expect(user.getUserById(2)).rejects.toEqual({
error: 'User id 2 does not exist'}); });Copy the code
async/await
We know that async/await is a Promise syntactic sugar for writing asynchronous code more elegantly, and jest also supports this syntax.
Let’s rewrite the above code:
// Test resolve with async/await
it('async/await to test resolve'.async () => {
expect.assertions(1);
const data = await user.getUserById(4);
return expect(data).toEqual('Cosen');
});
// Test async/await with reject
it('async/await to test reject'.async () => {
expect.assertions(1);
try {
await user.getUserById(2);
} catch (e) {
expect(e).toEqual({
error: 'User id 2 does not exist'}); }});Copy the code
⚠️ async does not return and uses try/catch to catch exceptions.
Mock
Before we get to mocks in Jest, let’s consider the following question: Why use mock functions?
In a project, it is common for methods in one module to call methods in another module. In unit tests, we might not care about the execution and results of an internally called method, just whether it was called correctly, and even specify the return value of that function. This is where mock comes in.
There are three main mock apis in Jest: jest.fn(), jest.mock(), and jest.spyon (). Using them to create mock functions can help us better test some of the more logically complex code in our projects. In our tests, we mainly used the following three features provided by mock functions:
- Capture function calls
- Sets the return value of the function
- Change the internal implementation of a function
Below, I’ll describe each of these methods and their application to real-world testing.
jest.fn()
Jest.fn () is the easiest way to create mock functions, and if the internal implementation of the function is not defined, jest.fn() returns undefined as the return value.
// functions.test.js
test('Test the jest. Fn () call'.() = > {
let mockFn = jest.fn();
let res = mockFn('xiamen'.'Qingdao'.'sanya');
// Assert that execution of mockFn returns undefined
expect(res).toBeUndefined();
// Assert mockFn is called
expect(mockFn).toBeCalled();
// Assert that mockFn is called once
expect(mockFn).toBeCalledTimes(1);
// Assert that mockFn passes in arguments 1, 2, 3
expect(mockFn).toHaveBeenCalledWith('xiamen'.'Qingdao'.'sanya');
})
Copy the code
The mock function created by jest.fn() can also set return values, define internal implementations, or return Promise objects.
// functions.test.js
test('Test jest.fn() returns fixed value'.() = > {
let mockFn = jest.fn().mockReturnValue('default');
Assert that mockFn returns a value of default after execution
expect(mockFn()).toBe('default');
})
test('Test the internal implementation of jest.fn()'.() = > {
let mockFn = jest.fn((num1, num2) = > {
return num1 + num2;
})
Return 20 after asserting mockFn execution
expect(mockFn(10.10)).toBe(20);
})
test('Test jest.fn() returns Promise'.async() = > {let mockFn = jest.fn().mockResolvedValue('default');
let res = await mockFn();
// assert mockFn returns default after execution with await keyword
expect(res).toBe('default');
// Assert that a mockFn call returns a Promise object
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
Copy the code
jest.mock()
In real projects, when testing asynchronous functions, you don’t actually send ajax requests to this interface. Why?
For example, 1W interfaces need to be tested, each interface needs 3s to return, and testing all interfaces needs 30000s, so the time of this automated test is too slow
We, as the front end, just need to confirm that the asynchronous request has been sent successfully, and we are in trouble as to what the back end interface returns, which is what the back end automation tests do.
Here’s a demo of an AXIos request:
// user.js
import axios from 'axios'
export const getUserList = () = > {
return axios.get('/users').then(res= > res.data)
}
Copy the code
Corresponding test file user.test.js:
import { getUserList } from '@/services/user.js'
import axios from 'axios'
/ / 👇 👇
jest.mock('axios')
/ / 👆 👆
test.only('test getUserList'.async () => {
axios.get.mockResolvedValue({ data: ['Cosen'.'forest'.'KeSen']})await getUserList().then(data= > {
expect(data).toBe(['Cosen'.'forest'.'KeSen'])})})Copy the code
Mock (‘axios’) we added jest. Mock (‘axios’) at the top of our test case. We told Jest to mock axios so that we didn’t ask for real data. When we call axios.get, instead of actually requesting the interface, we’ll simulate the result of a successful request with {data: [‘Cosen’,’ forest ‘,’ Cursen ‘]}.
Of course, it takes time to simulate asynchronous requests, which can be very long if there are many requests. In this case, you can create a __mocks__ folder in the root directory of your mock data. This way, instead of emulating Axios, you go directly to the native simulation method, which is more commonly used and won’t be explained here.
jest.spyOn()
The jest.spyon () method also creates a mock function, but the mock function not only captures the function invocation, but also executes the spy function normally. In fact, jest.spyon () is the syntactic sugar of jest.fn(), which creates a mock function with the same internal code as the spy function.
Snapshot Snapshot test
A snapshot is a snapshot. This usually involves automated testing of the UI, and the idea is to take a snapshot of the standard state at any given moment.
describe("XXX page".() = > {
// beforeEach(() => {
// jest.resetAllMocks()
// })
// Use snapshot for UI testing
it("The page should render normally.".() = > {
const wrapper = wrappedShallow()
expect(wrapper).toMatchSnapshot()
})
})
Copy the code
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 to the old one, and if the two are inconsistent, the test will fail, helping us ensure that the user interface does not change unexpectedly.
🎯 summary
That concludes some of the basic background on front-end unit testing and the basic APIS of Jest. In the next article, I’ll explain how to do component unit testing with a React component in my project.
📜 Reference link
- Segmentfault.com/a/119000001…