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

  1. define
  2. Installation and use
  3. Commonly used API
  4. Ground unit test
  5. Evolution: Building testable unit modules
  6. Maintainable unit modules
  7. review
  8. 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:

  1. Logical: The business logic is not clear, 1000+ rows
  2. Rely on:$dayjs, $API, $validate, $route, $echarts, mixins, $store.
  3. 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:

  1. Reuse: Complex business logic is enclosed in a black box for easier reuse.
  2. Quality: Module functionality is guaranteed by test cases.
  3. 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:

  1. Design: Split the business logic into unit modules (UI components, functional modules).
  2. 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: `
      
         < / SVG > `
       .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.