For a long time, unit testing is not a skill that front-end engineers should have. However, with the development of front-end engineering, the increasing complexity of projects and the pursuit of high reuse of code, unit testing becomes increasingly important and one of the key factors determining the quality of the whole project

1. What does unit testing mean?

  • In large-scale code refactoring, the correctness of the refactoring can be guaranteed
  • Ensure code quality and verify functional integrity

2. Understanding of mainstream front-end testing frameworks

2.1 Framework Comparison (top three mainstream)

  • Karma – A Node.js-based JavaScript Test Runner that automatically runs your code across multiple browsers (Chrome, Firefox, IE, etc.)
  • Mocha – Mocha is a testing framework that implements unit testing in VUE-CLI with the CHAI assertion library (Mocha+ CHAI)
  • Jest -Jest is a JavaScript testing framework developed by Facebook. It is widely used within Facebook to test all kinds of JavaScript code

2.2 Classification of unit tests

  • TDD – (Test-driven development) focuses on development, standardizing developers to write higher-quality, less buggy code through test cases
  • BDD – (Behavior-driven development) An outside-in approach to development in which business outcomes are defined externally to the point where they are realized, and each outcome is translated into a corresponding inclusion acceptance criteria

In simple terms, TDD writes the test module first, then writes the main function code, and then allows the test module to pass the test, while BDD is to write the main function module first, then writes the test module

2.3 assertions library

Assertions are Boolean expressions that are true at a particular point in a program and determine whether the actual result of code execution matches the expected result, while assertion libraries encapsulate commonly used methods

The main assertion libraries are

  • assert (TDD)
assert("mike" == user.name);
Copy the code
  • Expect. Js (BDD) – Expect () style assertion
expect(foo).to.be("aa");
Copy the code
  • The should.js-BDD (Behavior-driven development) style runs throughout
foo.should.be("aa"); //should
Copy the code
  • Chai (BDD/TDD) – integrates Expect (), Assert (), and Should-style assertions

3. Jest application of unit testing

Jest is an open-source JS unit testing framework from Facebook. It is also the unit testing framework used by React. Vue currently recommends Jest as a unit testing framework. In addition to Facebook, Twitter and Airbnb are also using Jest. In addition to basic assertions and Mock functionality, Jest also has utility features such as snapshot testing, real-time monitoring patterns, coverage reporting, and more. Jest also requires almost no configuration to use.

I used JEST as a unit test framework in project development, combined with vue-util-test, the official test tool of VUE

3.1 Jest installation

npm install --save-dev jest
npm install -g jest
Copy the code

3.2 Jest configuration file

(1) Adding method

  • Generate Jest. Config.js automatically
npx jest --init
Copy the code

And then there are some choices, based on their actual situation

Jest.config.js

  • Create and configure Jest. Config.js manually
const path = require('path');

module.exports = {
  verbose: true,
  rootDir: path.resolve(__dirname, '.. /.. /.. / '),
  moduleFileExtensions: [
    'js'.'json'.'vue',].testMatch: [
    '<rootDir>/src/test/unit/specs/*.spec.js',
  ], 
  transform: {
    '^.+\\.js$': 'babel-jest'.'.*\\.(vue)$': 'vue-jest',
  },
  moduleNameMapper: {
    '^ @ / (. *) $': '<rootDir>/src/$1',
  },
  transformIgnorePatterns: ['/node_modules/'],
  collectCoverage: false,
  coverageReporters: ['json'.'html'],
  coverageDirectory: '<rootDir>/src/test/unit/coverage', 
  collectCoverageFrom: [ 
    'src/components/**/*.(js|vue)'.'! src/main.js'.'! src/router/index.js'.'! **/node_modules/**',]};Copy the code

Configuration resolution:

  • TestMatch – Matches the test case file
  • The transform – usevue-jestTo deal with*.vueFile,babel-jestTo deal with*.jsfile
  • ModuleNameMapper – support the same in source code@ -> srcThe alias
  • CoverageDirectory – The directory where coverage reports are stored
  • CollectCoverageFrom – Test report wants to overwrite those files, directories, plus! Is to avoid these files

(2) Jest command line tool

{
  "name": "test"."version": "1.0.0"."scripts": {
    "unit": "jest --config src/test/unit/jest.conf.js --coverage",
  },
  dependencies": {"vue-jest":"^ 3.0.5"},"devDependencies": {"@vue/test-utils":"^ 1.0.0 - beta. 13","babel-core":"^ 7.0.0 - bridge. 0","babel-jest":"^ 21.2.0","jest":"^ 21.2.1".}}Copy the code
  • Config – Configures the jest configuration file path

  • Coverage – Generates test coverage reports

    Coverage is a command provided by Jest to generate a test coverage report. To generate a coverage report, add the –coverage parameter to package.json

(3) Unit test file name

It is named after spec. Js, which is short for sepcification. In testing terms, Specification refers to the technical details of a given feature or application that must be satisfied

(4) Unit test report coverage index

Run: NPM run unit

After the configuration, run this command to generate the coverage file and display the coverage summary of each indicator on the terminal

Open the Index.html in the Coverage directory on the web page to see the test report for each component

  • Statement coverage Is every statement executed?
  • Is every function called for branch coverage?
  • Function coverage Is every if block executed?
  • Is every line executed for line coverage?

When we complete the unit test coverage is less than 100%, do not panic, do not excessively pursue 100% coverage, the core function modules can be tested, of course, if you want to set the lowest coverage detection, you can add the following in the configuration, if the coverage is lower than the threshold you set (80%), the test result fails

//jest.config.js
coverageThreshold: {
    "global": {
      "branches": 80,
      "functions": 80,
      "lines": 80,
      "statements": 80}}.Copy the code

🚀 Official Documents

3.3 Common assertions of Jest

ToBe ({a: 1}); toBe({a: 1}); toBe({a: 1}); toBe({a: 1}); toBe({a: 1}); // Expect (1).tobe (2)// Expect (n).tobenull (); // Check whether null expect(n).tobetruthy (); // The result istrueexpect(n).toBeFalsy(); // The result isfalseExpect (value). ToBeCloseTo (0.3); // Expect (compileAndroidCode).tothrow (ConfigError); // Determine whether to throw an exceptionCopy the code

3.4 Jest + Vue Test Utils Test component instance

Vue Test Utils is the official unit testing utility library of VUe.js. It combines the two to Test captcha components and cover functional tests

//kAuthCode
<template>
    <p class="kauthcode">
        <span class="kauthcode_btn" v-if="vertification" @click="handleCode"</span> <span v-else>{{timer}} second </span> </p> </template> <script>export default {
  name: 'KAuthCode',
  props: {
    phone: {
      type: String,
      require: true,},type: {
      type: String,
      default: '1',
      require: true,
      validator(t) {
        return ['1'.'2'].includes(t); // 1 mobile phone 2 mailbox},}, validateType: {type: String,
      default: '1',
      validator(t) {
        return ['1'.'2'.'3'].includes(t); // 1 message 2 Form 3 custom},},},data() {
    return {
      timer: 60,
      vertification: true}; }, methods: {handleCode() {
      if(! this.phone) { switch (this.type) {case '1':
            this.$Message.warning('Mobile phone number cannot be empty');
            break;
          case '2':
            this.$refs.formRef.validateField('code');
            break;
          default: break;
        }
        return;
      }
      this.getCode();
    },
    getCode() {
      let response;
      switch (this.type) {
        case '1':
          response = this.$api.login.getPhoneCode({ mobileNumber: this.phone });
          break;
        case '2':
          response = this.$api.login.getEmailCode({ email: this.phone });
          break;
        default: break;
      }
      response.then(() => {
        this.$Message.success('Verification code sent successfully');
        this.vertification = false;
        const codeTimer = setInterval(() => {
          this.timer -= 1;
          if (this.timer <= 0) {
            this.vertification = true; this.timer = 60; clearInterval(codeTimer); }}, 1000); }); ,}}}; </script> <style lang="less" scoped>
    .kauthcode {
        span {
            display: inline-block;
            width: 100%;
        }
    }
</style>

Copy the code

The test file

// kAuthCode.spec.js
import {createLocalVue, mount, shallowMount} from '@vue/test-utils';
import KAuthCode from '@/components/common/KAuthCode.vue';
import login from '@/service/modules/login.js';
import iviewUI from 'view-design';

const localVue = createLocalVue();
localVue.use(iviewUI);
const testPhone = '18898538706';

jest.mock('@/service/modules/login.js', () => ({
    getPhoneCode: () => Promise.resolve({
        data: {
            answer: 'mock_yes',
            image: 'mock.png',
        }
    })
}))
describe('KAuthCode.vue', () => {
    const option = {
        propsData: {
            // phone: testPhone,
            type: '2'
        },
        mocks: {
            $api: {
                login
            },
        },
    };

    beforeEach(() => {
        jest.useFakeTimers();
    });
    afterEach(() => {
        jest.clearAllTimers();
    });

    const wrapper = mount(KAuthCode, option);
    
    it('Set mobile phone number', () => {
        const getCode = jest.fn();
        option.methods = {getCode};
        wrapper.find('.kauthcode_btn').trigger('click');
        expect(wrapper.vm.phone).toBe(testPhone);
    });

    it('Error reported if no mobile phone number is set', () => {
        wrapper.setData({type:'2'});
        const status = wrapper.find('.kauthcode_btn').trigger('click');
        expect(status).toBeFalsy();
    });
});

Copy the code

3.5 the Vue – Test – Utils API

3.5.1 track of Wrapper

A Wrapper is a Wrapper that contains a mounting component or vnode and a way to test that component or vnode by mounting the component with a mount(Component,option)

  • wrapper.vmAccess the actual Vue instance
  • wrapper.setDataModify the instance
  • wrapper.findFind (‘.kAuthCode_btn ‘).trigger(‘click’);
  • propsData– Props setting when a component is mounted
import {createLocalVue, mount, shallowMount} from '@vue/test-utils';
import KAuthCode from '@/components/common/KAuthCode.vue';

const option = {
        propsData: {
            // phone: testPhone,
            type: '2'
        },
        mocks: {
            $api: {
                login
            },
        },
    };
    
const wrapper = mount(KAuthCode, option);

Copy the code

Ps: You can also use shallowMount to mount components. The difference is that shallowMount does not render subcomponents. You can use shallowMount and mount methods to mount the same component and test the snapshot to view the content of the generated file

3.5.2 CreateLocalVue

Returns a Vue class where you can add components, mix in, and install plug-ins without contaminating the global Vue class

import {createLocalVue, mount} from '@vue/test-utils';
import iviewUI from 'view-design';

const localVue = createLocalVue();
localVue.use(iviewUI);

Copy the code

3.5.3 Testing hooks

BeforeEach and afterEach – Within the same Describe description, beforeAll and afterAll are executed in multiple IT scopes and are suitable for one-time Settings

  • BeforeEach (fn) what needs to be done beforeEach test, such as restoring some data to its initial state before a test
  • AfterEach (FN) runs at the end of each test case execution
  • BeforeAll (fn) what needs to be done beforeAll tests
  • AfterAll (FN) runs after the test case execution is complete

Call order: beforeAll => beforeEach => afterAll => afterEach

 beforeEach(() => {
        jest.useFakeTimers();
    });
    afterEach(() => {
        jest.clearAllTimers();
    });
Copy the code

3.5.4 mock function

The three apis related to Mock functions are jest.fn(), jest.spyon (), and jest.mock().

  • Jest.fn () – is the easiest way to create Mock functions. If you don’t define an internal implementation of the function, jest.fn() returns undefined. You can also set a return value, define an internal implementation, or return a Promise object, as in the following example:
// assert that mockFn returns name it('jest.fn() return value ', () = > {let mockFn = jest.fn().mockReturnValue('name');
  expect(mockFn()).toBe('name'); }) // Define the internal implementation of jest.fn() and assert its result it(Internal implementation of 'jest.fn() ', () = > {let mockFn = jest.fn((a, b) => {
    returna + b; }) expect(mockFn(2, 2)).toBe(4); }) //jest.fn() returns the Promise object it('jest. Fn () returns the Promise', async () => {
  let mockFn = jest.fn().mockResolvedValue('name');
  letresult = await mockFn(); // assert mockFn returns name expect(result).tobe ('name'); / / assertion mockFn call returns after Promise Object expect (Object. The prototype. ToString. Call (mockFn ())). The place ("[object Promise]");
})
Copy the code
  • Jest. Mock () – jest. Mock automatically organizes mock objects based on the module being mocked. The mock object will have the fields and methods of the original module
// kAuthCode.spec.js

    jest.mock('@/service/modules/login.js', () => ({
       getPhoneCode: () => Promise.resolve({
           data: {
               answer: 'mock_yes',
               image: 'mock.png',
           }
       })
   }))

  it('Set mobile phone number', () => {
       const getCode = jest.fn();
       option.methods = {getCode};
       wrapper.find('.kauthcode_btn').trigger('click');
       expect(getCode).toHaveBeenCalled()
       expect(wrapper.vm.phone).toBe(testPhone);
   });
   
Copy the code

To mock out the entire axios request, use toHaveBeenCalled to determine whether the method is called

In this case, we’ll just focus on the getCode method and ignore the rest. To test this approach, we should do:

  • We don’t need to actually call the axios.get method; we need to mock it out
  • We need to test if the AXIos method is called (but not actually fired) and returns a Promise object
  • The returned Promise object executes the callback function

Note: there may be situations where the same method is called in the same component but returns different values, and we may mock it multiple times. Use the restoreAllMocks method to reset the state beforeEach

Purpose of mock:

  • Sets the return value of the function
  • Gets the function call
  • Change the internal implementation of the original function

4. ️ Step pit 🏆

1. Trigger event - Suppose the component library uses the @change event provided in iView for <Checkbox>, but when we do the wrapper.trigger('change'), is not triggered. There is also a difference between @click() in <Button> and @click in <Button>. 2. Rendering issues - The rendered HTML of the component provided by the component library may be different from the HTML you see from the wrapper.html() console. To avoid errors, console. Log the wrapper.html() to see the actual renderingCopy the code

🚀 Vue Test Utils official website configuration