Article tone
- Introduce concepts and thinking process, without providing code (for specific code writing, please refer to jest official website )
extend:
- In the era of information explosion, all kinds of resources are very rich, and there are many materials on the Internet for specific tutorials
- The details are only on the official website, and the same information will not be repeated, causing additional mental burden
- The brain is just a search engine, it knows where to find resources, it is not responsible for recording specific practices, saving memory
several names for the test
- Visual test: [Test tool] The front-end vision is more variable, so the cost of visual test is high and the popularity is not high, but the advantage is that it can test style information
- Unit test: [Test target] The smallest granularity test, for a single function or function, suitable for the test of function library, basic component library, etc.
- Integration testing: [Test target] Simulate user operations, deliver the final result, and target the project process
- TDD (Test Driven Development): [Methodology] First write test cases (put forward expectations), then write specific implementation methods and functions, and apply them to unit testing
- BDD (Behavior Driven Development): [Methodology] Based on Integration Testing
This article mainly introduces the jest (joke) unit testing library
Principles and limitations of jest unit testing
first introduces the principle, hoping to let everyone know its functional boundaries, what it can do, what it can't do, and understand the scope of capabilities
Jest runs on the node side, and the underlying implementation library is jsdom. Node is used to simulate a set of dom environment. The scope of the simulation is limited to the dom hierarchy and operation.
[DOM operation] Only simulate most common functions of DOM, some specific DOM APIs are not supported, such as canvas, video media function API
- If you want to test the media API of canvas and video, you need to install the corresponding extension library, which can be understood as implementing browser functions on the node side, such as image generation, audio and video playback, etc.
- Canvas extension , video related extension not found
[css style] Strictly speaking, there is no css style simulation function, and css is only regarded as a pure dom attribute string in jsdom, which is no different from id and class strings.
- Inheritance is not supported, each dom is an independent entity, and there is no style inheritance.
- only supports inline styles , cannot recognize styles in vue
- Less useful example of parsing external link styles
- There is a solution for here , but it doesn't get an official merge
- Non-inline style tests, need to use visual test library
Which scenarios do unit tests need to cover?
code changes
- You can find out by running unit tests directly, but how do you prevent developers from forgetting to run unit tests?
- Solved by adding the cicd process, when submitting a merge request application, the unit test is triggered, and if the operation fails, the merge request is automatically rejected, and the node command is executed to send a message reminder
- gitlab ci related configuration will be introduced at the end of the article
add code
- The newly added function or function will not be covered by running the old unit test. How to remind the developer to cover the newly added part of the code?
- It is solved by configuring the number of test coverage lines to be 100%. If the target is not reached, the test will be regarded as failing to avoid the omission of new code. How to solve a branch or function that cannot be covered?
- By configuring "ignore remarks / istanbul ignore next / ", keep a 100% coverage test of a file
If you have time in the future, you can also search for these ignored configurations globally to cover the tests one by one and play the role of markers.
coverageThreshold: { './src/common/js/*.js': { branches: 80, // 覆盖代码逻辑分支的百分比 functions: 80, // 覆盖函数的百分比 lines: 80, // 覆盖代码行的百分比 statements: -10 // 如果有超过 10 个未覆盖的语句,则 jest 将失败 } },
Add a new file, whether the test is missing
- Under normal circumstances, the unit test will only run the unit test file. The newly added code file does not have the corresponding test file, and there will be missed tests.
Specify the folder to be covered by the
collectCoverageFrom
parameter. When the file in the folder does not have a corresponding test case, it will be treated as a coverage rate of 0, which will serve as a reminder for missing new files.// 从那些文件夹中生成覆盖率信息,包括未设置对其编写测试用例的文件,解决遗漏新文件的测试覆盖问题 collectCoverageFrom: [ './src/common/js/*.{js,jsx}', './src/components/**/*.{js,vue}', ],
Special Scenarios (Experience Value)
- For some functions, there is no problem in running them under normal circumstances, and errors will be reported only in special cases, such as simple addition operations, if they are placed in decimals, there will be calculation errors,
0.1 + 0.2 = 0.30000000000000004
- The coverage of these special scenarios can only be recorded by front-line developers in actual work, which takes time to accumulate
- This is the value of programmer experience, and it is also a rare part worth inheriting
- For some functions, there is no problem in running them under normal circumstances, and errors will be reported only in special cases, such as simple addition operations, if they are placed in decimals, there will be calculation errors,
Unit testing ignores the principle
The bottom layer of jest collection coverage uses the istanbul library (istanbul: Istanbul, Shengchan carpet, carpet for coverage), the following ignore formats are the functions of the istanbul library
- Ignore this file and put it at the top of the file / istanbul ignore file /
- Ignore a function, a piece of branch logic or a line of code, put it at the top of the function / istanbul ignore next /
- Ignore function parameter default value
function getWeekRange(/* istanbul ignore next */ date = new Date()) {
- For specific ignore rules, please refer to istanbul github introduction
Correct posture for writing test cases
takes the expectation and positioning of the function as the starting point, not the code. At the beginning, you should think about the function that the function or tool library needs to play, instead of looking at the code at the beginning
- First, list what you expect, the function of the component or function, and write it out in text. This is also the function described in
test ('detecting click events'), and inform others of the purpose of this test case
- Write corresponding test cases
- Modify the code that does not satisfy the test case
- Watch code coverage, cover all lines of code
Add jest global custom function
- If the frequency of a certain test function is relatively high, you can consider alignment and reuse, write a preload file, and load the file before each test file is executed.
- For example, it is cumbersome to obtain the original code of dom style,
wrapper.element.style.height
, and element has not been officially exposed, it is an internal variable You can add a configuration file, write a global method of styles, and obtain style data through a function, which is consistent with methods such as classes
// jest.config.js 设置前置运行文件,在每个测试文件执行前,会运行该文件,实现添加某些全局方法的作用 setupFilesAfterEnv: ['./jest.setup.js'],
// ./jest.setup.js import { shallowMount } from '@vue/test-utils' // 向全局 wrapper 挂载通用函数 styles,返回该元素的内联样式(因为 jsdom 只支持内联样式,不支持检测 class 中的样式),或某内联样式的值 function addStylesFun() { // 生成一个临时组件,获取 vueWrapper 及 domWrapper 实例,挂载 style 方法 const vueWrapper = shallowMount({ template: '<div>componentForCreateWrapper</div>' }) const domWrapper = vueWrapper.find('div') vueWrapper.__proto__.styles = function(styleName) { return styleName ? this.element.style[styleName] : this.element.style } domWrapper.__proto__.styles = function(styleName) { return styleName ? this.element.style[styleName] : this.element.style } } addStylesFun()
hook function
is similar to the guard function in vue router, it executes the hook function before and after entering
- Solve the data storage problem of stateful functions and avoid repeatedly writing code to prepare data when executing each test case
beforeAll、afterAll
- Written in the outermost part of the unit test file, it means that the function is executed once before and after the file is executed.
- Written in the outermost layer of the test group describe, which means that the function is executed once before and after the test group is executed.
beforeEach、afterEach
- Each test case (test) is executed once before and after
Quick Unit Testing Tips
skips use cases that have been successfully tested and the source code has not been changed, and no longer executes redundantly
The first step, the jest --watchAll test file has changed, the test will be executed automatically
- This parameter can only be added to the package script command, and it will not take effect after the npm command.
- Source code changes, or unit test file changes, will trigger
The second step, press f (only execute the wrong use case)
- The disadvantage is that it is impossible to monitor the changes of successfully executed unit tests, as well as the changes of the corresponding source code, (that is, those that have been successful before will be ignored, regardless of whether new changes have occurred or errors have occurred)
- Source code changes, or unit test file changes, will trigger
- Global traversal can be toggled by pressing f repeatedly
The third step, press o again (only execute the test case of the file whose source code has changed)
- equivalent to jest --watch
- Only listen to files in git that are not submitted to the staging area. Once the stash is submitted, it will no longer be triggered
- Even if there is a failing test case in that file, it will be ignored
- Press a to run the test cases of all files, that is, the switch between a and o
- The bottom layer is to distinguish files by reading the contents of the .git folder, so it depends on the existence of git
Press w to display the menu to see watch options
general, the set o and f are used, first o (ignore the unchanged file, when we change the file, it will be monitored. Then press f repeatedly, only monitor the wrong use case)
jest report description
- Hover the corresponding chart with the mouse to display the corresponding prompt
- "5x" means that the statement was executed 5 times during the test
- "I" is the test case if condition is not entered, that is, there is no test case with if true
"E" is the case when the test case does not test the if condition is false
- That is, the if condition in the test case is always true, and a test case with the if condition is false must be written, that is, the E will disappear without entering the code in the if condition.
simulates the function , not a function of the simulated data
- Just mock functions (Function, jest.fn()), not functions that generate mock data like mockjs
effect:
- Check how many times the function has been executed
- Detects the this pointing to when the function is executed
- Detect input parameters during execution
- Check the return value after execution
Override mock third-party functions
- Override the axios function to avoid actually initiating the interface and customize the specific return value
jest.mock('axios'); axios.get.mockResolvedValue(resp);
- There's no magic in it, no private adaptation, just pure function overloading. Equivalent to
axios.get = ()=> resp
rewrites the method
- Override the axios function to avoid actually initiating the interface and customize the specific return value
Ultimate method, covering the entire 3rd party library
- Write an avatar file. When importing with import, the avatar file is imported
- You can also use jest.requireActual('../foo-bar-baz') to force the import to be the real file instead of the alias file
timer emulation
- Override the setTimeout timer, you can skip the specified time and shorten the unit test running time
test snapshot
- Snapshot, that is, a data copy, that is, to detect whether the "current data" is the same as the "old data copy", similar to
JSON.stringify()
, to serialize the data Application scenarios
- Restricting configuration file changes
- Detect the comparison of the dom structure, whether the change of a function affects the dom structure
- In general, it is used for comparison operations on big data to avoid writing data in unit test files.
Other incurable diseases
Aliases and Equivalent Methods
- it is an alias for test, the two are equivalent
- toBeTruthy !== toBe(true), toBeFalsy !== toBe(false), toBe(true) is more strict, toBeTruthy is whether it is true after forcibly converted to boolean
- skip skips a test case, which is more elegant than comments
describe.skip('test custom directive',xxx) `test.skip('test custom directive',xxx)`
jest toBe, uses Object.is internally for comparison
- The difference from === is that it behaves the same as the triple equals operator except for NaN, +0 and -0
- Solve the problem of decimal floating point calculation error
toBeCloseTo
Asynchronous testing, force verification of promises through .resolves / .rejects and go to a specific branch
test('the data is peanut butter', () => { return expect(fetchData()).resolves.toBe('peanut butter'); });
Solve the problem of overwriting the default parameter of new Date
test('当前月,测试参数 new Date 默认值', () => { // 覆写 new Date 的值,模拟为 2022/01/23 17:13:03 ,解决默认参数为 new Date 时,无法覆盖的问题 const mockDate = new Date(1642938133000) const spyDate = jest .spyOn(global, 'Date') // 即监听 global.Date 变量 .mockImplementationOnce(() => { spyDate.mockRestore() // 需要在第一次执行后,马上消除该 mock,避免后续影响后续 new Date return mockDate }) let [starTime, endTime] = getMonthRange() expect(starTime).toBe(1640966400000) // 2022/01/01 00:00:00 expect(endTime).toBe(1643644799000) // 2022/01/31 23:59:59 })
Equivalent to writing in native syntax
const OriginDate = global.Date Date = jest.fn(() => { Date = OriginDate return new OriginDate(1642938133000) })
Use the latest syntax
beforeAll(() => { jest.useFakeTimers('modern') jest.setSystemTime(new Date(1466424490000)) // 因为 Vue Test Utils 中使用的 jest 是 24.9 的版本,没有该函数 }) afterEach(() => { jest.restoreAllMocks() })
Matching tests, and using multiple batches of data to run the same test case
describe.each([ [1, 1, 2], // 每一行是代表运行一次测试用例 [1, 2, 3], // 每一行中的参数,是运行该次测试用例是用到的数据,前两个是参数,第三个是测试期望值 [2, 1, 3], ])( '.add(%i, %i)', // 设置 describe 的标题,%i 是 print 的变量参数 (a, b, expected) => { test(`returns ${expected}`, () => { expect(a + b).toBe(expected); }); });
gitlab-ci unit test related configuration
- Trigger ci to execute unit tests when a merge request is made
When the unit test fails, execute the node file and send the Feishu information. The Feishu information includes the link of the merge request. You can click the link to quickly locate the unit test job and view the problem.
stages: - merge-unit-test - merge-unit-test-fail-callback - other-test # merge 请求时执行的 job step-merge: stage: merge-unit-test # 使用的 gitlab runner tags: [front-end] # 仅在提出代码合并请求时执行 only: [merge_requests] # 排除特定分支的代码合并请求,即在特定分支的代码合并请求时,不执行该 job except: variables: - $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "qa" # 运行的命令 script: - npm install --registry=https://registry.npm.taobao.org # 安装依赖 # 2>&1 标准错误定向到标准输出 # Linux tee 命令用于读取标准输入的数据,并将其内容输出成文件。 - npm run test 2>&1 | tee ci-merge-unit-test.log # 执行单元测试,并将在控制台输出的信息,保存在 ci-merge-unit-test.log 文件中,以便后续分析 - echo 'merge-unit-test-finish' # 定义往下一个 job 需要传递的资料 artifacts: when: on_failure # 默认情况下,只会在 success 保存,可以通过这个标识符进行配置 paths: # 定义需要传递的文件 - ci-merge-unit-test.log # merge 检测失败时执行的 node 命令 step-merge-unit-test-fail-callback: stage: merge-unit-test-fail-callback # 当上一个 job 执行失败时,才会触发 when: on_failure tags: [front-end] only: [merge_requests] except: variables: - $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "qa" script: - node ci-merge-unit-test-fail-callback.js $CI_PROJECT_NAME $CI_JOB_ID # 执行 node 脚本,进行飞书通知,并携带对应链接,进行快速定位
ci-merge-unit-test-fail-callback.js.js
const fs = require('fs') const path = require('path') const https = require('https') const projectName = process.argv[2] // 项目名 const jobsId = process.argv[3] // 执行的 ci 任务 id const logsMainMsg = fs.readFileSync(path.join(__dirname, 'ci-merge-unit-test.log')) .toString() .split('\n') .filter(line => line[line.length - 1] !== '|' && line.indexOf('PASS ') !== 0) // 过滤不关注的信息 .join('\n') const data = JSON.stringify({ msg_type: 'post', content: { post: { zh_cn: { content: [ [ { tag: 'a', text: 'gitlab merge 单元测试', href: `https://xxx/fe-team/${projectName}/-/jobs/${Number(jobsId) - 1}` }, { tag: 'text', text: `运行失败\r\n${logsMainMsg}` } ] ] } } } }) const req = https.request({ hostname: 'open.feishu.cn', port: 443, path: '/open-apis/bot/v2/hook/xxx', method: 'POST', headers: { 'Content-Type': 'application/json' } }, res => { console.log(`statusCode: ${res.statusCode}`) res.on('data', d => process.stdout.write(d)) }) req.on('error', error => console.error(error)) req.write(data) req.end()
grateful
- Recently, the output of articles is small, there are too many things, and I am lazy
- Thanks to the netizens for their concern and supervision, it feels so good to be missed
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。