Introduction to difficult points of Jest unit test

Article tone

  • Introduce the concept and thinking process, and do not provide code (refer to the specific code writing method) jest official website)
  • Extension:

    • In the era of information explosion, all kinds of resources are very rich. There are many materials on the Internet
    • However, the details of the official website do not duplicate the same information, resulting in additional mental burden
    • The brain is just a search engine. It knows where to find resources and is not responsible for recording specific practices to save memory

Several names of test

  • Visual test: [test tool] the front-end vision is relatively changeable, so the cost of visual test is large and the popularity is not high, but the advantage is that it can test the style information
  • Unit test: [test objective] the test of minimum granularity, which is suitable for the test of function library, basic component library, etc. for a single function or function
  • Integration test: [test objective] simulate the user's operation, face the final result of delivery, and focus on the process of the project
  • TDD (Test Driven Development): [methodology] first write test cases (put forward expectations), and then write specific implementation methods and functions for unit testing
  • BDD (Behavior Driven Development): [methodology] Based on integration testing
  • This paper mainly introduces jest (joke) unit test library

Principles and limitations of jest unit testing

I hope you can understand the principle, what the scope is and whether you can do it first

  • jest runs on the node side. The underlying implementation library is jsdom. Node is used to simulate a set of dom environment. The scope of simulation is limited to dom hierarchy and operation

  • [dom operation] it only simulates most of the general functions of dom, but some specific dom APIs do not support it, such as the media function api of canvas and video

    • 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 realizing the functions of the browser on the node side, such as image generation, audio and video playback, etc
    • canvas extension , video related extensions are not found yet
  • [css style] strictly speaking, there is no css style simulation function. css is only regarded as a pure dom attribute string in jsdom, which is no different from id and class strings

What scenarios do unit tests need to cover?

  • Code change

    • You can find it by running unit tests directly, but how to avoid developers forgetting to run unit tests?
    • It is solved by adding cicd process. When submitting the merge request application, the unit test is triggered. If the operation fails, the merge request is automatically rejected, and the node command is executed to send a message reminder
    • gitlab ci configuration will be introduced at the end of the article
  • New code

    • New functions or functions will not be covered by running old unit tests. How to remind developers to cover the newly added code?
    • Solve the problem by configuring 100% of the test coverage lines. If the target is not reached, it will be regarded as failing the test to avoid the omission of new code. How to solve branches or functions that cannot be covered?
    • By configuring "ignore comments / istanbul ignore next /", maintain the 100% coverage test of a file
    • If you have time later, you can also search these ignore configurations globally to cover the tests one by one and play the role of marking

      coverageThreshold: {
       './src/common/js/*.js': {
         branches: 80, // Percentage of code logical branches covered
         functions: 80, // Percentage of override function
         lines: 80, // Percentage of code lines covered
         statements: -10 // If there are more than 10 uncovered statements, jest will fail
       }
      },
  • Add new documents and whether the test is omitted

    • In general, unit tests only run unit test files. If there is no corresponding test file in the newly added code file, there will be missed tests
    • Specify the folder to be overwritten through the collectCoverageFrom parameter. When there is no corresponding test case for the file in the folder, it will be treated as coverage 0 to remind the missing test of new files

      // Generate coverage information from those folders, including files for which test cases are not set, and solve the test coverage problem of missing new files
      collectCoverageFrom: [
        './src/common/js/*.{js,jsx}',
        './src/components/**/*.{js,vue}',
      ],
  • Special scenarios (value of experience)

    • For some functions, there is no problem in normal operation, and errors will be reported only in special cases. For example, for simple addition operation, there will be calculation error in decimal, 0.1 + 0.2 = 0.300000000000000 4
    • The coverage of these special scenes can only be recorded by front-line developers in their actual work, which requires the accumulation of time
    • This is the value of programmer experience, and it is also a rare part that is worth inheriting

Unit test ignore principle

The istanbul library is used at the bottom of jest collection coverage (istanbul: istanbul, Shengsheng carpet, carpet for coverage). The following ignored formats are the functions of istanbul library

  • Ignore this file and put it at the top of the file/
  • Ignore a function, a piece of branch logic or a line of code and put it at the top of the function / istanbul ignore next/
  • Ignore function parameter default value function getWeekRange(/* istanbul ignore next */ date = new Date()){
  • Specific ignore rules can be viewed Introduction to istanbul github

Correct posture for writing test cases

Take the expectation and positioning of the function as the starting point, not the code. At the beginning, we should first think about the functions that the function or tool library needs to play, rather than the code at the beginning

  • First list the functions of the component or function you expect, and write it in text. This is also the function described in test('detect click event ') to inform others of the purpose of this test case
  • Write corresponding test cases
  • Modify the code that does not meet the test case
  • Observe code coverage and cover all code lines

Add jest global custom function

  • If the frequency of a test function is relatively high, you can consider aligning and reusing it, write a preloaded 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, which is an internal variable
  • You can add configuration files, write styles global methods, and obtain style data through functions, which is consistent with methods such as classes

    // jest.config.js sets the pre run file, which will be run before each test file is executed to add some global methods
    setupFilesAfterEnv: ['./jest.setup.js'],
    // ./jest.setup.js
    import { shallowMount } from '@vue/test-utils'
    
    // Mount the general-purpose function styles to the global wrapper and return the inline style of the element (because jsdom only supports inline styles and does not support detecting styles in class), or the value of an inline style
    function addStylesFun() {
      // Generate a temporary component, obtain vueWrapper and domWrapper instances, and mount the style method
      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

Similar to the guard function in vue router, the hook function is executed before and after entering

  • Solve the problem of data storage of stateful functions and avoid repeatedly writing code to prepare data when executing each test case
  • beforeAll,afterAll

    • If it is 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
    • It is 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 is executed once before and after

Quick unit testing skills

Skip the use cases that have been tested successfully and the source code has not changed, and there is no need to execute them

  • In the first step, if the jest --watchAll test file changes, the test will be executed automatically

    • This parameter can only be added in the package script command. It will not take effect after the npm command is executed
    • Source code change or unit test file change will trigger
  • Step 2, press f (execute only the wrong use case)

    • The disadvantage is that the changes of the successfully executed unit tests and the corresponding source code cannot be monitored (that is, the previously successful ones will be ignored, regardless of the new changes and whether there are errors)
    • Source code change or unit test file change will trigger
    • You can switch the global traversal by pressing f repeatedly
  • Step 3, press o again (only execute the test case of the file whose source code has changed)

    • Equivalent to jest --watch
    • Only listen to the files in git that are not submitted to the temporary storage area. Once the stash is submitted, it will not be triggered
    • Even if there are failed test cases in this file, they will be ignored
    • Press o to switch the test case file with a
    • The bottom layer is read through The contents of git folder are used to distinguish files, so it depends on the existence of git
  • Press w to display the menu and view the options of watch
    In general, set o and F to use, first O (ignore the unchanged file, and it will be monitored when we change the file. Then press f repeatedly to only listen for the wrong use case)


jest report description

  • Hover the mouse over the corresponding chart to display the corresponding prompt
  • "5x" indicates that this statement has been executed 5 times in the test
  • 怌1怍 If condition of test case is not entered, I.e. no test case with true if
  • 怌E怍 This is the case when the test case does not test if condition is false

    • That is, the if condition in the test case is always true. You have to write a test case whose if condition is false, that is, you don't enter the code in the if condition, and this E will disappear

Analog function , not a function of analog data

  • It is only an analog Function (Function, jest.fn()), not a Function that generates analog data like mockjs
  • effect:

    • Detect how many times the function has been executed
    • Detect the point of this when the function is executed
    • Input parameters during detection execution
    • Return value after detection execution
  • Overlay simulation third-party function

    • Override the Axios function, avoid actually initiating the interface, and customize the specific return value jest mock('axios'); axios. get. mockResolvedValue(resp);
    • There is no magic or private adaptation, just a simple function overload. Equivalent to Axios Get = () = > resp overrides this method
  • The ultimate way to cover the entire third-party library

    • Write the avatar file. When importing with import, the avatar file is imported
    • You can also use jest Requireactual ('.. / foo bar Baz') to force the imported file to be a real file without using a avatar file

Timer simulation

  • Copy the setTimeout timer. You can skip the specified time and shorten the running time of the unit test

Test snapshot

  • Snapshot, that is, data copy, that is, to detect whether the "current data" is the same as the "old data copy", similar to JSON Stringify() to serialize and record data
  • Application scenario

    • Restrict changes to configuration files
    • Check the comparison of dom structure and whether the change of a function affects the dom structure
    • Generally speaking, it is used for the comparison of big data to avoid writing the data in the unit test file

Other difficult and miscellaneous diseases

  • Alias and equivalent method

    • it is the alias of test, and the two are equivalent
    • toBeTruthy !== toBe(true),toBeFalsy !== toBe(false) and tobe (true) are more strict. Tobetrust is whether it is true after being strongly converted to boolean
    • Skip skipping a test case is more elegant than annotation Skip ('test custom instruction ', xxx) ` test Custom command ('xxx ', skip test')`
  • jest toBe, internal use object Is for comparison

    • The difference from = = = is that it behaves the same as the triple sign operator except NaN, + 0 and - 0
  • Solve the calculation error of decimal point floating point number toBeCloseTo
  • Asynchronous test, passed resolves / . Rejections force the verification promise to take a specific branch

    test('the data is peanut butter', () => {
      return expect(fetchData()).resolves.toBe('peanut butter');
    });
  • Solve the overwrite problem with the default parameter of new Date

    test('Current month, test parameters new Date Default value', () => {
      // Overwrite the value of new Date, simulated as 2022 / 01 / 23 17:13:03, to solve the problem that it cannot be overwritten when the default parameter is new Date
      const mockDate = new Date(1642938133000)
      const spyDate = jest
        .spyOn(global, 'Date') // That is, monitor global Date variable
        .mockImplementationOnce(() => {
          spyDate.mockRestore() // You need to eliminate the mock immediately after the first execution to avoid subsequent impact on subsequent new dates
          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 using native syntax to write

      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)) // Because jest used in Vue Test Utils is version 24.9, there is no such function
        })
      
        afterEach(() => {
          jest.restoreAllMocks()
        })
  • Match test, and run the same test case using the data of multiple batches

    describe.each([
      [1, 1, 2], // Each line represents a test case run
      [1, 2, 3], // The parameters in each line are the data used to run the test case. The first two are parameters and the third is the expected value of the test
      [2, 1, 3],
    ])(
      '.add(%i, %i)', // Set the title of describe,% i is the variable parameter of print
     (a, b, expected) => {
      test(`returns ${expected}`, () => {
        expect(a + b).toBe(expected);
      });
    });

Gitlab CI unit test related configuration

  • When a merge request is initiated, the ci is triggered to execute the unit test
  • When the unit test fails, execute the node file and send the flybook information. The flybook 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
    
    # job executed when merge is requested
    step-merge:
    stage: merge-unit-test
    
    # gitlab runner used
    tags: [front-end]
    
    # Execute only when a code merge request is made
    only: [merge_requests]
    
    # Exclude the code merge request of a specific branch, that is, the job will not be executed when the code merge request of a specific branch
    except:
      variables:
        - $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "qa"
    
    # Command to run
    script:
      - npm install --registry=https://registry.npm.taobao.org # installation dependency
      # 2> & 1standard error directed to standard output
      # The Linux tee command is used to read the standard input data and output its contents to a file.
      - npm run test 2>&1 | tee ci-merge-unit-test.log # Execute the unit test and save the information output on the console in CI merge unit test Log file for subsequent analysis
      - echo 'merge-unit-test-finish'
    
    # Define the data to be transferred to the next job
    artifacts:
      when: on_failure # By default, it will only be saved in success and can be configured through this identifier
      paths:  # Define the files to be transferred
        - ci-merge-unit-test.log
    
    # node command executed when merge detection fails
    step-merge-unit-test-fail-callback:
    stage: merge-unit-test-fail-callback
    
    # It is triggered only when the previous job fails to execute
    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 # Execute the node script, notify the flying book, and carry the corresponding link for quick positioning
    
  • 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] // Project name
    const jobsId = process.argv[3] // id of ci task executed
    
    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) // Filter information you don't care about
    .join('\n')
    
    const data = JSON.stringify({
    msg_type: 'post',
    content: {
      post: {
        zh_cn: {
          content: [
            [
              {
                tag: 'a',
                text: 'gitlab merge unit testing ',
                href: `https://xxx/fe-team/${projectName}/-/jobs/${Number(jobsId) - 1}`
              },
              {
                tag: 'text',
                text: `Run failed\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()
    

thank

  • The output of recent articles is less, there are too many things, and I'm lazy
  • Thank you for your concern and supervision. It feels good to be concerned

Keywords: Javascript unit testing Jest

Added by jonoc33 on Fri, 11 Feb 2022 11:37:27 +0200