Have been interested in single measurement, but for single coverage, test report and other keyword ability, how in recent months has been groping in the Vue ground unit tests in the business system, see slowly increase the coverage, slowly clear module, the understanding of the unit test is deeper than before, there are also some experience and harvest.
Today, I would like to share my notes to communicate with you some ideas and methods of single test on the ground in two relatively complex Vue business systems. It can be regarded as the notes of entry practice, and senior executives please skip them.
The outline
- define
- Installation and use
- Commonly used API
- Ground unit test
- Evolution: Building testable unit modules
- Maintainable unit modules
- review
- Discuss && ‘Thank
Definition 1.
Unit test definition:
Unit testing is the inspection and verification of the smallest testable unit in software. Unit is a very important part in quality assurance. According to the principle of the test pyramid, the higher the test is, the larger the proportion of the test investment will be, and the worse the effect will be. The cost of unit test is much smaller, and it is easier to find problems.
There are also different test stratification strategies (ice cream model, champion model).
2. Installation and use
1. Add @vue/unit-jest to the vue projectThe document
$ vue add @vue/unit-jest
Copy the code
After the installation is complete, the test:unit script option will be added to package.json and the jest.config.js file will be generated.
// package.json
{
"name": "avatar"."scripts": {
"test:unit": "vue-cli-service test:unit".// New script
},
"dependencies": {... },"devDependencies": {... }},Copy the code
Scripts to generate test reports: Add the — Coverage custom parameter
// package.json
{
"name": "avatar"."scripts": {
"test:unit": "vue-cli-service test:unit"."test:unitc": "vue-cli-service test:unit --coverage".// Test and generate test report
},
"dependencies": {... },"devDependencies": {... }},Copy the code
2. Configure the VScode vscode-jest-Runner plug-in
What it does: After VS Code opens the test file, it can run the use case directly.Operation effect:Fail effect:
Installing a plug-in: marketplace.visualstudio.com/items?itemN… Configuration item: Set => jest-runner Config
- **/*.{test,spec}.{js, JSX,ts, TSX}
- Jest Command: Defines the Jest Command. The default is the Jest global Command.
Replace Jest Command with Test :unit and use the Test :unit provided by the Vue scaffold for unit testing.
3. Githook configuration
Function: Execute all test cases at commit time, cancel commit if test cases fail or coverage is not up to standard.
Installation:
$ npm install husky --save-dev
Copy the code
Configuration:
// package.json
{
"name": "avatar"."scripts": {
"test:unit": "vue-cli-service test:unit"."test:unitc": "vue-cli-service test:unit --coverage".// Test and generate test report
},
"husky": {
"hooks": {
"pre-commit": "npm run test:unitc" // Execute parameter unit tests at commit and generate test reports}}},Copy the code
Set traction index: jest. Config. js, can be set globally, set to folders, set to a single file.
module.exports = {
preset: '@vue/cli-plugin-unit-jest'.timers: 'fake'.coverageThreshold: {
global: { / / global
branches: 10.functions: 10.lines: 10.statements: 10
},
'./src/common/**/*.js': { / / folder
branches: 0.statements: 0
},
'./src/common/agoraClientUtils.js': { // Single file
branches: 80.functions: 80.lines: 80.statements: 80}}}Copy the code
4. Test report
The generated test report is under the Coverage folder in the following directory, with four main indicators.
- Statement Coverage Whether each statement is executed
- Branch Coverage Whether each if block is executed
- Function coverage Whether each function is called
- Line Coverage Whether each line is executed
Screenshot from the root directoryThree colors represent three states: red, yellow, and green.Single file screenshot: the red behavior is not overwritten, and the green behavior runs times.
3. Commonly used API
This is a good introduction, showing only simple usage, see the documentation for details.
Jest common method: documentation
/ / case
describe('versionToNum version number to number '.() = > {
it('10.2.3 = > 10.2'.() = > {
expect(versionToNum('10.2.3')).toBe(10.2)
})
it('11.2.3 = > 11.2'.() = > {
expect(versionToNum('11.2.3')).toBe(11.2)})})/ * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
/ / value contrast
expect(2 + 2).toBe(4);
expect(operationServe.operationPower).toBe(true)
// Object comparison
expect(data).toEqual({one: 1.two: 2});
/ / JSON
expect(data).toStrictEqual(afterJson)
// Before each execution
beforeEach(() = > {
// do some thing....
/ / DOM Settings
document.body.innerHTML = `
`
})
// Mock
const getCondition = jest.fn().mockImplementation(() = > Promise.resolve({ ret: 0.content: [{ parameterName: 'hulala'}]}))/ / Promise
it('Get preset buried points - pages'.() = > {
return getCondition('hz'.'pages').then(() = > {
// If the logType does not contain presetEvent and is not equal to pages, obtain the preset buried point
expect($api.analysis.findPresetList).toBeCalled()
})
// Timer method
it('Timer new execution'.() = > {
const timer = new IntervalStore()
const callback = jest.fn()
timer.start('oneset', callback, 2000)
expect(callback).not.toBeCalled()
jest.runTimersToTime(2000) // Wait 2 seconds
expect(callback).toBeCalled()
})
Copy the code
@vue/test-utils Common methods: documentation
/ / case
import { mount } from '@vue/test-utils'
import Counter from './counter'
describe('Counter'.() = > {
// Now mount the component and you get the wrapper
const wrapper = mount(Counter)
it('renders the correct markup'.() = > {
expect(wrapper.html()).toContain('<span class="count">0</span>')})// It is also easy to check existing elements
it('has a button'.() = > {
expect(wrapper.contains('button')).toBe(true)})})/ * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
import { shallowMount, mount, render, renderToString, createLocalVue } from '@vue/test-utils'
import Component from '.. /HelloWorld.vue'
/ / the router simulation
import VueRouter from 'vue-router'
const localVue = createLocalVue()
localVue.use(VueRouter)
shallowMount(Component, { localVue })
/ / fake
const $route = {
path: '/some/path'
}
const wrapper = shallowMount(Component, {
mocks: {
$route
}
})
/ / store simulation
const store = new Vuex.Store({
state: {},
actions
})
shallowMount(Component, { localVue, store })
it('Error message display'.async() = > {// shallowMount parameter simulation
const wrapper = shallowMount(cloudPhone, {
propsData: {
mosaicStatus: false.customerOnLine: true.cloudPhoneState: false.cloudPhoneError: true.cloudPhoneTip: 'An error occurred'.delay: ' '}})// Whether the child component is displayed
expect(wrapper.getComponent(Tip).exists()).toBe(true)
/ / HTML
expect(wrapper.html().includes('An error occurred')).toBe(true)
// DOM element judgment
expect(wrapper.get('.mosaicStatus').isVisible()).toBe(true)
// Execute the click event
await wrapper.find('button').trigger('click')
// class
expect(wrapper.classes()).toContain('bar')
expect(wrapper.classes('bar')).toBe(true)
// Child component lookup
wrapper.findComponent(Bar)
/ / destroy
wrapper.destroy()
//
wrapper.setData({ foo: 'bar' })
/ / axios simulation
jest.mock('axios'.() = > ({
get: Promise.resolve('value')}})))Copy the code
4. Ground unit test
❌ Adding a unit test directly to a larger business component requires simulating a series of global functions that cannot be run directly.
Question:
- Logical: The business logic is not clear, 1000+ rows
- Rely on:
$dayjs, $API, $validate, $route, $echarts, mixins, $store
. - Inconsistent paths: Yes
@
,. /
,../
Unit tests are tests that verify the correctness of a module, function, or class. — Liao Xuefeng’s official website
The ground:
✅ Extract pure functions, class methods, components, and add separate test code for business logic key points.
Example: Get group parameters aggregated by seven interfaces.
Original logic: Global variables are stored for system parameters and global variables are stored for user-defined parameters
- You can’t tell how many types and how many interfaces there are
- Cannot be directly multiplexed in multiple locations
getCondition (fIndex, oneFunnel) { // Add a constraint if the event is not pulled first
const {biz, logType, event, feCreateType} = oneFunnel
return new Promise((resolve, reject) = > {
// If the private constraint is empty and is not a preset event or page group, pull the private constraint
try {
this.$set(this.extraParamsList.parameterList, fIndex, {})
if(logType ! = ='pages' && logType.indexOf('presetEvent') = = = -1) {
this.$api.analysis[`${logType}ParameterList`] ({biz: logType === 'server' && feCreateType === 0 ? ' ' : biz,
event: event,
terminal: this.customType[logType],
platform: logType === 'server' && feCreateType === 0 ? 'common' : ' '.pageNum: -1
}).then(res= > {
if (res.ret === 0) {
res.content.forEach(element= > {
this.$set(this.extraParamsList.parameterList[fIndex], element.parameterName || element.parameter_name, element)
})
resolve()
} else {
reject('Failed to obtain event properties, please contact the background administrator.')}}}else if ((logType === 'presetEvents' || logType === 'presetEventsApp')) {
this.$api.analysis.findPresetList({
biz,
appTerminal: logType,
operation: event
}).then(res= > {
if (res.code === 0) {
res.data.forEach(item= > {
item.description = item.name
this.$set(this.extraParamsList.parameterList[fIndex], item.name, item)
})
resolve()
}
})
} else {
resolve('No pull')}}catch (e) {
reject(e)
}
})
},
getGlobalCondition (funnelId) { // Get the global base option
return new Promise((resolve, reject) = > {
this.$api.analysis.getGlobalCondition({
funnelId: funnelId,
type: this.conditionMode
}).then(res= > {
if (res.code === 0) {
const {bizList, expressions, expressionsNumber, comBizList} = res.data
this.bizList = Object.assign(... bizList)this.comBizList = Object.assign(... comBizList)this.comBizKeyList = Object.keys(this.comBizList)
this.operatorList = expressions
this.numberOperatorList = expressionsNumber
this.comBizKey = Object.keys(this.comBizList)
this.getComBizEvent()
resolve(res)
} else {
this.$message.error('Failed to get base option, please contact background administrator')
reject('Failed to get base option, please contact background administrator')
}
})
})
},
setCommonPropertiesList (data) { // Initialize the public restriction list commonPropertiesList
const commonPropertiesList = {
auto: data.h5AutoCommonProperties,
pages: data.h5PagesCommonProperties,
presetEvents: data.h5PresetCommonProperties, // h5 presets event public properties
customH5: data.h5CustomCommonProperties,
customApp: data.appCustomCommonProperties,
presetEventsApp: data.appPresetCommonProperties, // App presets event public properties
server: data.serverCommonProperties,
customWeapp: data.weappCustomCommonProperties,
presetEventsWeapp: data.weappPresetCommonProperties, / / event Weapp preset public properties
presetEventsServer: data.serverPresetCommonProperties || [], // Server presets event public properties
presetEventsAd: data.adPresetCommonProperties
}
for (let type in commonPropertiesList) { // Combine the value of parameter_name as key and item as value in the k-V format
let properties = {}
if(! commonPropertiesList[type])continue
commonPropertiesList[type].forEach(item= > {
properties[item.parameter_name] = item
})
commonPropertiesList[type] = properties
}
this.commonPropertiesList = commonPropertiesList
},
Copy the code
After splitting the module: Create the GetParamsServer main class, which consists of two subclasses, and aggregate the subclass interface.
This is one of the subclasses that gets a unit test for private parameters:
import GetParamsServer, { GetPrivateParamsServer } from '@/views/analysis/components/getParamsServer.js'
describe('GetPrivateParamsServer Private parameter '.() = > {
let $api
beforeEach(() = > {
$api = {
analysis: {
findPresetList: jest.fn().mockImplementation(() = > Promise.resolve({
code: 0.data: [{ name: 'hulala'.description: '234234'.data_type: 'event'}]})),// Presets the buried point
serverParameterList: jest.fn().mockImplementation(() = > Promise.resolve({
ret: 0.content: [{ parameterName: 'hulala'}]})),// Server buried point
autoParameterList: jest.fn().mockImplementation(() = > Promise.resolve({
ret: 0.content: [{ parameter_name: 'hulala'}]})),// H5 full buried point
customH5ParameterList: jest.fn().mockImplementation(() = > Promise.resolve({
ret: 0.content: [{ parameterName: 'hulala'}]})),// H5 custom
customWeappParameterList: jest.fn().mockImplementation(() = > Promise.resolve({
ret: 0.content: [{ parameter_name: 'hulala'.description: '234234'.data_type: 'event'}]})),/ / Weapp custom
customAppParameterList: jest.fn().mockImplementation(() = > Promise.resolve({
ret: 0.content: [{ parameterName: 'hulala'.description: 'asdfafd'.data_type: 'event'}]}))// App custom
}
}
})
describe('GetPrivateParamsServer of different types'.() = > {
it('Get preset buried points - pages'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getCondition('hz'.'pages').then(() = > {
// If the logType does not contain presetEvent and is not equal to pages, obtain the preset buried point
expect($api.analysis.findPresetList).toBeCalled()
})
})
it('Get preset buried point - presetEvent'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getCondition('hz'.'presetEvent').then(() = > {
// If the logType does not contain presetEvent and is not equal to pages, obtain the preset buried point
expect($api.analysis.findPresetList).toBeCalled()
})
})
it('Get non-preset buried points - Others'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getCondition('hz'.'12312').then(() = > {
expect($api.analysis.findPresetList).not.toBeCalled()
})
})
it('Get non-preset buried point-server'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getCondition('hz'.'server').then(() = > {
expect($api.analysis.serverParameterList).toBeCalled()
})
})
it('Get non-preset buried point -auto'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getCondition('hz'.'auto').then(() = > {
expect($api.analysis.autoParameterList).toBeCalled()
})
})
it('Get non-preset buried point - customH5'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getCondition('hz'.'customH5').then(() = > {
expect($api.analysis.customH5ParameterList).toBeCalled()
})
})
it('get buried the preset point - customWeapp'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getCondition('hz'.'customWeapp').then(() = > {
expect($api.analysis.customWeappParameterList).toBeCalled()
})
})
it('Get non-preset buried points - customApp'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getCondition('hz'.'customApp').then(() = > {
expect($api.analysis.customAppParameterList).toBeCalled()
})
})
it('Get non-preset buried point - non-existent type'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getCondition('hz'.'Ha ha ha ha').then(res= > {
expect(res.length).toBe(0)
})
})
})
describe('GetPrivateParamsServer results to label'.() = > {
it('Get preset buried points - pages'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getConditionLabel('hz'.'pages').then((res) = > {
expect(res.length).toBe(1) expect(!! res[0].value).toBeTruthy() expect(!! res[0].label).toBeTruthy()
expect(res[0].types).toBe('custom')
expect(res[0].dataType).toBe('event')
})
})
it('get buried the preset point - customWeapp'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getConditionLabel('hz'.'customWeapp').then((res) = > {
expect(res.length).toBe(1) expect(!! res[0].value).toBeTruthy() expect(!! res[0].label).toBeTruthy()
expect(res[0].types).toBe('custom')
expect(res[0].dataType).toBe('event')
})
})
it('Get non-preset buried points - customApp'.() = > {
const paramsServer = new GetPrivateParamsServer()
paramsServer.initApi($api)
return paramsServer.getConditionLabel('hz'.'customApp').then((res) = > {
expect(res.length).toBe(1) expect(!! res[0].value).toBeTruthy() expect(!! res[0].label).toBeTruthy()
expect(res[0].types).toBe('custom')
expect(res[0].dataType).toBe('event')})})})Copy the code
Code logic from the test case:
- Six interface
- There are six event types
- Mapping between types and interfaces
- There are three interface formats
Function:
- Reuse: Complex business logic is enclosed in a black box for easier reuse.
- Quality: Module functionality is guaranteed by test cases.
- Maintenance: Tests are documentation for understanding business logic.
Practice: In the process of adding single test, abstract modules, refactor some functions, and add single test to modules with single responsibility.
5. Evolution: Building testable unit modules
Evolving business code code into testable code, focusing on:
- Design: Split the business logic into unit modules (UI components, functional modules).
- Time: feasible refactoring goals and methods, with long-term refactoring expectations.
Designing test cases for modules with a single responsibility will provide more comprehensive coverage, so the design step is particularly important.
If the way to save a system is to redesign a new one, is there any reason to think that it would be better to start from scratch? — How to Organize Neatly
The original module is also designed, how can we ensure that the refactoring is really better than before? Or it should be judged objectively according to the design principles.
Design principles SOLID:
- SRP- Single responsibility
- OCP- Open and closed: easy and extensible, resistant to modification.
- Lsp-richter replacement: Subclass interfaces are unified and interchangeable.
- ISP- Interface isolation: don’t rely on what you don’t need.
- Dip-dependency reversal: Build A stable abstraction layer with one-way dependencies (example: A => B => C, counter example: A => B => C => A).
In front of the overwhelming demand, but also to dismantle modules, refactoring, single test, is undoubtedly an increase in workload, it seems impractical, “refactoring” the book gave me a lot of guidance.
Reconstruction method:
- Preparatory reconstruction
- Refactoring to help with understanding
- Garbage refactoring (Camp rule: refactor as much as you come across as garbage, leaving your code cleaner and healthier than when you arrived)
- Planned refactoring versus opportunistic refactoring
- Long-term reconstruction
Module and UI combing of business system 1:
Module and UI combing of business system 2:
6. Maintainable unit modules
Avoid writing bad-smelling code after refactoring and extract specifications that are cheaper to implement.
Code bad smell:
- Cryptic naming – The inability to pick out a good name may be underlying a deeper design problem.
- Duplicate code
- Too long function – small function, pure function.
- Too long parameters
- Global data – The difficulty of processing increases exponentially as the volume increases.
- Variable data – Unknown at which node the data was modified.
- Divergent variation – Focuses only on the current modification, not on other associations.
- Shotgun change – The change code is scattered around.
- Attachment – communicating data with external modules over internal data.
- Data muddle – The same parameter is passed between multiple functions.
- Basic type of paranoia
- Repetition of the switch
- Looping statements
- Redundant elements
- Talk about generality
- Temporary field
- Too long message chain
- A middleman
- Insider trading
- Too much class
- Similar classes
- Pure data classes
- Rejected bequest – Inherits useless properties or methods from a parent class
- Comments – When you feel the need to write comments, try refactoring first, trying to make all comments redundant.
Specification:
- Number of global variables: 20 ±
- Methods The number of rows: 15 ±
- Lines of code: 300-500
- Internal methods, inline methods: start with an underscore
Skills:
- Use class syntax: Encapsulate closely related methods and variables.
- Use the Eventemitter tool library: Implement simple publish subscriptions.
- Use the vue provide syntax: pass the instance.
- Use the koroFileHeader plug-in: Uniform Annotation specification.
- Use the Git-commit plugin: Unify the commit specification.
- Use ESLint + stylelint (no variables, false name changes, debugger, automatically optimized CSS).
Example code:
/* * @name: lightweight message prompt plugin * @description: mimicking iView's $message method, API is consistent with style. * /
class Message {
constructor() {
this._prefixCls = 'i-message-';
this._default = {
top: 16.duration: 2}}info(options) {
return this._message('info', options);
}
success(options) {
return this._message('success', options);
}
warning(options) {
return this._message('warning', options);
}
error(options) {
return this._message('error', options);
}
loading(options) {
return this._message('loading', options);
}
config({ top = this._default.top, duration = this._default.duration }) {
this._default = {
top,
duration
}
this._setContentBoxTop()
}
destroy() {
const boxId = 'messageBox'
const contentBox = document.querySelector(The '#' + boxId)
if (contentBox) {
document.body.removeChild(contentBox)
}
this._resetDefault()
}
/ * * *@description: Render message *@param {String} Type type *@param {Object | String} Options Detailed format */
_message(type, options) {
if (typeof options === 'string') {
options = {
content: options
};
}
return this._render(options.content, options.duration, type, options.onClose, options.closable);
}
/ * * *@description: Render message *@param {String} Content Message content *@param {Number} Duration Duration *@param {String} Type Message type */
_render(content = ' ', duration = this._default.duration, type = 'info',
onClose = () => { }, closable = false
) {
// Obtain node information
const messageDOM = this._getMsgHtml(type, content, closable)
// Insert the parent container
const contentBox = this._getContentBox()
contentBox.appendChild(messageDOM);
// Delete method
const remove = () = > this._removeMsg(contentBox, messageDOM, onClose)
let removeTimer
if(duration ! = =0){
removeTimer = setTimeout(remove, duration * 1000);
}
// Close button
closable && this._addClosBtn(messageDOM, remove, removeTimer)
}
/ * * *@description: Deletes the message *@param {Element} ContentBox parent node *@param {Element} MessageDOM Message node *@param {Number} Duration Duration */
_removeMsg(contentBox, messageDOM, onClose) {
messageDOM.className = `The ${this._prefixCls}box animate__animated animate__fadeOutUp`
messageDOM.style.height = 0
setTimeout(() = > {
contentBox.removeChild(messageDOM)
onClose()
}, 400);
}
/ * * *@description: Gets the icon *@param {String} type
* @return {String} DOM HTML string */
_getIcon(type = 'info') {
const map = {
info: `
`.success: `
.warning: `
`.error: `
.loading: `
}
return map[type]
}
/ * * *@description: Gets the message node *@param {String} Type type *@param {String} Content Message content *@return {Element} Node DOM object */
_getMsgHtml(type, content) {
const messageDOM = document.createElement("div")
messageDOM.className = `The ${this._prefixCls}box animate__animated animate__fadeInDown`
messageDOM.style.height = 36 + 'px'
messageDOM.innerHTML = `
<div class="The ${this._prefixCls}message" >
The ${this._getIcon(type)}
<div class="The ${this._prefixCls}content-text">${content}</div>
</div>
`
return messageDOM
}
/ * * *@description: Adds a close button *@param {Element} MessageDOM Message node DOM */
_addClosBtn(messageDOM, remove, removeTimer) {
const svgStr = `<svg class="The ${this._prefixCls}btn" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>`
const closBtn = new DOMParser().parseFromString(svgStr, 'text/html').body.childNodes[0];
closBtn.onclick = () = > {
removeTimer && clearTimeout(removeTimer)
remove()
}
messageDOM.querySelector(`.The ${this._prefixCls}message`).appendChild(closBtn)
}
/ * * *@description: Obtain the container * of the parent node@return {Element} Node DOM object */
_getContentBox() {
const boxId = 'messageBox'
if (document.querySelector(The '#' + boxId)) {
return document.querySelector(The '#' + boxId)
} else {
const contentBox = document.createElement("div")
contentBox.id = boxId
contentBox.style.top = this._default.top + 'px'
document.body.appendChild(contentBox)
return contentBox
}
}
/ * * *@description: resets the height of the parent node */
_setContentBoxTop() {
const boxId = 'messageBox'
const contentBox = document.querySelector(The '#' + boxId)
if (contentBox) {
contentBox.style.top = this._default.top + 'px'}}/ * * *@description: restores the default value */
_resetDefault() {
this._default = {
top: 16.duration: 2}}}if (typeof module! = ='undefined' && typeof module.exports ! = ='undefined') {
module.exports = new Message();
} else {
window.$message = new Message();
}
Copy the code
6. Review
- define
- Installation and use (installation, debugging, Git interception, test report)
- Common apis (JEST, VUE components)
- Ground unit test (split key modules plus single test)
- Evolution: Building testable unit modules (design principles, refactoring)
- Maintainable unit modules (code specifications)
Ground line:
① Installation and use => ② API learning => ③ Landing: split key modules and single test => ④ Evolution: Architecture design and refactoring => ⑤ Code specification
The future:
⑥ Document first (to be explored)
In the more complex business system development process, from the first version of the code to gradually divide modules, add single test, or take a detours. It’s much more efficient to get into the document-first habit of designing modules and test cases before writing code.
To understand:
- Unit testing has long term value and execution costs.
- Good architecture design is the ground for single testing. It is easier to design single tests and add unit tests for modules with a single responsibility.
- The business form and phase of each project are different, and may not be suitable for each project. Find the balance point suitable for the project.
7. Discuss && Thank you
Thank you for seeing the end, the first half is dry, the second half is water, for internal sharing notes, part of the code and pictures have been processed, focus on sharing and communication with everyone, I sincerely hope that you can agree, if you have any gains, please click “like” to collect.