preface

In the last article, we explained the basic theory of single test and vUE single test introduction. The so-called journey of a thousand miles begins with a single step. We have completed the installation to the introduction and completed the first single test case. In this article, we will explore best practices and specifications for unit testing in VUE

Use Chrome for single-test code debugging

In the process of writing unit tests, we cannot avoid some code errors. At this time, we can only locate code problems through the command line or report. Then how to simulate the running environment of the test and debug the tested code? Node + Chrome provides this capability as follows:

1. Add the debugger where you need to debug in your code

// increment.spec.js
// Import the test toolset
import { mount } from "@vue/test-utils";
import Increment from "@/views/Increment";

describe("Increment".() = > {
  // Mount the component to get the wrapper
  const wrapper = mount(Increment);
  const vm = wrapper.vm;
  // simulate a user click
  it("button click should increment the count".() = > {
    expect(vm.count).toBe(0);
    const button = wrapper.find("button");
    debugger;
    button.trigger("click");
    expect(vm.count).toBe(1);
  });
});
Copy the code

2. Add the debug script to the scripts of package.json

"test:unit:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --no-cache --runInBand".Copy the code

3. Run the following command

npm run test:unit:debug
Copy the code

4. Open Chrome and click the debug icon to open the debug window

Click the Start debugging button and you will have the same experience as client-side code debugging

Components in VUE

Whether the vue or react, ng project, there is the concept of component, the component is usually divided into UI component, business component, the different types of components, for the emphasis of the test is not the same, general UI components are more focused on the interface and interaction of generality, the reuse of components may be more focused on business functions, the application of large is often happened, Over time, UI logic and business logic become more and more confused, with scattered concerns leading to low coverage, while unit testing forces you to separate UI logic from business logic, which is beneficial for unit testing, so it’s important to keep the two separate.

Good unit tests must follow the AIR principles:

  1. A-automatic (Principle of automation)

Unit tests should run automatically, validate automatically, and give results automatically

  1. I-independent (Principle of independence)

Unit tests should be run independently, with no dependencies on each other, external resources, and multiple runs

  1. R-repeatable (Repeatable principle)

Unit tests are repeatable, with consistent results each time

UI components

UI component, also known as basic component, generally refers to the encapsulation of basic style, logic, interaction and general ability, and maintains certain expansibility and universality. In order to improve the development efficiency, front-end students generally choose common UI libraries, such as ElementUI, AntDesign, or the internal component library of the company. Of course, As the project progresses, components within the team are usually precipitated. They are the smallest granularity of front-end components and are not affected by business, so they are called basic components

Pursuing line-level coverage is not recommended for UI components, as it can lead us to focus too much on the internal implementation details of the component, leading to trivial testing. Instead, we recommend writing tests as public interfaces that assert your component and handling them inside a black box. A simple test case will assert whether some input (user interaction or prop change) provided to a component results in the desired result (render result or trigger custom event).

The business component

Business component generally refers to a set of business functions encapsulated, which might involve multiple UI components, mainly for a single or multiple projects in a similar scenario reuse, business components in order to reduce the repeated work in the process of business code development, to encapsulate business logic, according to the needs of the business to provide certain extensibility, loose coupling, flattening of data structure, etc.

limitations

Unit testing is not necessarily suitable for all components, each project, not suitable for things to do but counterproductive, so, before you write unit tests first consider to be clear about this component should write unit tests, such as code inside filled with low granularity, high coupling code, you will find that you need to spend plenty of time to complete coverage, It would take a lot of time to maintain two sets of code, so we weighed the pros and cons and considered writing unit tests for the more stable, core components of the system

How to test components

An important question for a component to test is, what does this component do, and what are its inputs and outputs? Once the inputs and outputs are identified, we have a test direction. We can think of components as black boxes or functions that take inputs, form outputs, and test the effect of different inputs on the outputs. This is the core point.

UI components

  • The life cycle
  • Input parameter props
  • Render text test
  • Event interaction testing
  • Asynchronous test

The business component

  • The life cycle
  • Input parameter props
  • Render text test
  • Dom test
  • Inline style tests involving logic
  • Event interaction testing
  • The mock data
  • Asynchronous test
  • Process simulation
  • Vuex test
  • Route simulation test

Principle: 60% statement coverage; Core modules should have at least 80% statement coverage and 80% branch coverage

Vue Test Utils provides a detailed description of the Test scope mentioned above. You can refer to the Vue Test Utils interface for implementation.

Vue Test Utils documentation address: vue-test-utils.vuejs.org/zh/

Implement case

UI components
UI Component instance

MyButton.vue

<template>
  <button :class="['my-button', `my-button--${type}`, { 'my-button--disabled': disabled }]" :disabled="disabled" @click="handleClick">
    <i :class="icon" v-if="icon"></i>
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>

<script>
export default {
  name: "my-button".props: {
    type: {
      type: String.default: "default",},disabled: {
      type: Boolean.default: false,},icon: {
      type: String.default: "",}},methods: {
    handleClick(evt) {
      this.$emit("click", evt); ,}}};</script>

<style scoped lang="less">
.my-button {
  border: 1px solid #dcdfe6;
  color: # 606266;
  white-space: nowrap;
  cursor: pointer;
  background: #fff;
  display: inline-block;
  padding: 12px 20px;
  font-size: 14px;
  border-radius: 4px;
}

.my-button-primary {
  color: #fff;
  background-color: #409eff;
  border-color: #409eff;
}

.my-button--success {
  color: #fff;
  background-color: #67c23a;
  border-color: #67c23a;
}

.my-button--disabled {
  cursor: not-allowed;
}
</style>
Copy the code

MyButton.spec.js

import { mount } from "@vue/test-utils";
import MyButton from "@/components/MyButton";

describe("Button".() = > {
  it("render button".() = > {
    const wrapper = mount(MyButton, {
      slots: {
        default: "ok",}});const button = wrapper.find("button");
    expect(button.exists()).toBe(true);
    expect(button.text()).toBe("ok");
    expect(button.classes("my-button--default")).toBe(true);
  });

  it("render a primary button".() = > {
    const wrapper = mount(MyButton, {
      slots: {
        default: "ok",},propsData: {
        type: "primary",}});const button = wrapper.find("button");
    expect(button.classes("my-button--primary")).toBe(true);
  });

  it("render a disabled button".() = > {
    const wrapper = mount(MyButton, {
      slots: {
        default: "ok",},propsData: {
        disabled: true,}});const button = wrapper.find("button");
    expect(button.classes("my-button--disabled")).toBe(true);
    expect(button.attributes("disabled")).toBe("disabled");
  });

  it("icon".() = > {
    const wrapper = mount(MyButton, {
      slots: {
        default: "ok",},propsData: {
        icon: "correct",}});const correct = wrapper.find("button .correct");
    expect(correct.exists()).toBe(true);
  });

  it("click".async() = > {const wrapper = mount(MyButton, {
      slots: {
        default: "ok",}});const button = wrapper.find("button");
    await button.trigger("click");
    expect(wrapper.emitted().click).toBeTruthy();
  });
});
Copy the code
UI component running results

The business component
Component analysis

This is a selected version of the component, as shown below:

  1. The default:

  1. Mouse over:

  1. Click Edit:

  1. Click Delete to emit an event externally
Test ideas
  • Assert the influence of props on page rendering, overriding conditional branch statements related to props, assert that vm._data changed
  • Simulate mouse over scene, trigger event mouseEnter, assert DOM render, vm._data change
  • Simulate mouse moving out of scene, trigger event mouseleave, assert DOM rendering, vm._data change
  • Simulate the mouseEnter scene, triggering the event mouseEnter
  • Simulate clicking the edit button, triggering the event Click, which asserts that the relevant VM._data changes
  • Trigger the custom event “visible-change” to assert that the relevant VM._data has changed
  • Trigger the click event for the drop-down Item, asserting that the custom event “updatePackageVersion” is triggered
  • Simulate the search input box input, simulate the “change” event that triggers the input, and assert that vm._data has changed
  • Simulate clicking the delete button and assert that the custom event “deletePackage” is fired
The instance

Dependency.vue

<template>
  <div class="dependency" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
    <p class="dependency-selected">
      <span class="dependency-name">{{ pkg.name }}</span>
      <span class="dependency-version" v-if=! "" clientId || version ! == '' || appType === 2">
        <span class="dependency-latest">{{ version }}</span>
      </span>
    </p>
    <div v-if="isInOperation && ((pkg.key ! == 'platform' && appType === 2) || appType === 0)" class="dependency-operations">
      <span class="dependency-operation" @click="handleDeletePackage"><i class="mp-icon-trash-2"></i></span>
      <el-dropdown trigger="click" @command="handleCommand" @visible-change="handleVisibleChange">
        <span class="dependency-operation" ref="setting"><i class="mp-icon-edit-2"></i></span>
        <el-dropdown-menu slot="dropdown" class="versions-menu">
          <li class="app-version-filter">
            <el-input
              placeholder="Version Number"
              v-model="versionKeywords"
              icon="search"
              debounce="500"
              :on-icon-click="handleVersionsKeywordChange"
              @change="handleVersionsKeywordChange"
            >
            </el-input>
          </li>
          <li class="versions-menu-options">
            <ul class="versions-sub-menu">
              <el-dropdown-item
                :class="[{ 'dep-selected': option.ver === version }]"
                :command="option"
                v-for="(option, index) in stableVersions"
                :key="index"
                >{{ option.ver }}</el-dropdown-item
              >
            </ul>
          </li>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>

<script>
export default {
  name: 'application-dependency'.props: {
    appType: Number.clientId: String.pkg: Object
  },
  data() {
    return {
      versionKeywords: ' '.defaultVersions: [].stableVersions: [].version: ' '.latest: ' '.stable: ' '.beta: ' '.isOperationShow: false.isInOperation: false.currentVersion: ' '.type: 0.types: [{label: 'Latest version'.value: 0
        },
        {
          label: 'Private beta'.value: 2}}},computed: {
    versions() {
      const pkg = this.pkg
      let versions = []
      if (pkg.versions) {
        versions = pkg.versions
      } else {
        if (pkg.version) {
          versions = [
            {
              state: 5.ver: pkg.version
            }
          ]
        }
      }

      return versions
    }
  },
  watch: {
    versions() {
      this.stableVersions = this.getStableVersions()
    }
  },
  created() {
    const stableVersions = this.getStableVersions()
    const latest = stableVersions[0]
    const pkg = this.pkg

    this.stableVersions = stableVersions
    this.defaultVersions = stableVersions

    if (latest) {
      if (pkg.key === 'platform') {
        this.version = pkg.currentVersion || latest.ver
      } else {
        this.version = latest.ver
      }
    }
  },
  methods: {
    getStableVersions() {
      if (this.versions.length === 0) {
        return this.versions
      }
      return this.versions.filter(version= > version.state === 5)},handleVersionsKeywordChange() {
      this.stableVersions = this.defaultVersions.filter(version= > version.ver.indexOf(this.versionKeywords) > -1)},handleVisibleChange(visible) {
      if (visible) {
        this.isInOperation = true
        this.isOperationShow = true
      } else {
        this.isInOperation = false
        this.isOperationShow = false
        this.stableVersions = []
        this.versionKeywords = ' '}},handleCommand(cmd) {
      this.version = cmd.ver

      this.$emit('updatePackageVersion', {
        key: this.pkg.key,
        version: this.version,
        isLatest: false})},handleMouseEnter() {
      this.isInOperation = true
    },
    handleMouseLeave() {
      if (!this.isOperationShow) {
        this.isInOperation = false}},handleDeletePackage() {
      this.$emit('deletePackage'.this.pkg.key)
    }
  }
}
</script>
Copy the code

Dependency.spec.js

// Import the test toolset
import { mount, createLocalVue } from '@vue/test-utils'
import Dependency from '@/components/Dependency'
import ElementUI from 'element-ui'
const localVue = createLocalVue()
localVue.use(ElementUI)

describe('Dependency'.() = > {
  it('Test render component text'.() = > {
    const pkg = {
      key: 'platform'.appId: 60531.name: 'You're so handsome.'.version: 'latest'.currentVersion: null.versions: [{ver: '3.0.5.24'.state: 5
        },
        {
          ver: '3.0.5.23'.state: 5
        },
        {
          ver: '3.0.5.22'.state: 5}}]const wrapper = mount(Dependency, {
      propsData: {
        pkg
      },
      localVue
    })
    // expect(wrapper.element).toMatchSnapshot()
    expect(wrapper.find('.dependency-name').text()).toBe(pkg.name)
    expect(wrapper.find('.dependency-latest').text()).toBe('3.0.5.24')
    expect(wrapper.vm.versions.length).toBe(3)
    expect(wrapper.vm.stableVersions.length).toBe(3)
  })

  it('PKg. versions does not exist, version list is the current PKG. version, length 1'.() = > {
    const pkg = {
      key: 'platform'.appId: 60531.name: 'You're so handsome.'.version: 'latest'.currentVersion: null
    }
    const wrapper = mount(Dependency, {
      propsData: {
        pkg
      },
      localVue
    })

    expect(wrapper.vm.versions.length).toBe(1)
  })

  it('pkg.key does not equal platform'.() = > {
    const pkg = {
      key: 'product'.appId: 60531.name: 'products'.version: 'latest'.currentVersion: null.versions: [{ver: '3.0.5.24'.state: 5
        },
        {
          ver: '3.0.5.23'.state: 5
        },
        {
          ver: '3.0.5.22'.state: 5}}]const wrapper = mount(Dependency, {
      propsData: {
        pkg
      },
      localVue
    })

    expect(wrapper.vm.version).toBe('3.0.5.24')
  })

  it('clientId exists, version does not exist, appType does not equal 2, version text will not be rendered '.() = > {
    const pkg = {
      key: 'platform'.appId: 60531.name: 'You're so handsome.'.version: 'latest'.currentVersion: null.versions: []}const wrapper = mount(Dependency, {
      propsData: {
        pkg,
        clientId: '1111111'.appType: 1
      },
      localVue
    })
    expect(wrapper.find('.dependency-version').exists()).toBe(false)
  })

  it('After MouseEnter: Render dropDown, click modify button, click Select Version, Search, click Delete button'.async() = > {const wrapper = getMouseEnterWrapper()

    const vm = wrapper.vm

    const dependencyDiv = wrapper.find('.dependency')

    / / triggers mouseenter
    await dependencyDiv.trigger('mouseenter')

    expect(vm.isInOperation).toBe(true)

    expect(wrapper.find('.dependency-operations').exists()).toBe(true)

    await dependencyDiv.trigger('mouseleave')

    expect(vm.isInOperation).toBe(false)

    / / triggers mouseenter
    await dependencyDiv.trigger('mouseenter')

    const elDropDown = wrapper.findComponent({ name: 'el-dropdown' })

    // Click the modify button
    elDropDown.vm.$emit('visible-change'.true)

    expect(vm.isOperationShow).toBe(true)

    const menuItem = wrapper.find('.versions-sub-menu li')

    await menuItem.trigger('click')

    // Emit to update external data
    expect(wrapper.emitted().updatePackageVersion).toBeTruthy()

    elDropDown.vm.$emit('visible-change'.false)

    expect(vm.isInOperation).toBe(false)
    expect(vm.isOperationShow).toBe(false)
    expect(vm.stableVersions.length).toBe(0)
    expect(vm.versionKeywords).toBe(' ')

    const elInput = wrapper.findComponent({ name: 'el-input' })

    vm.versionKeywords = '3.0.5.23'

    elInput.vm.$emit('change')

    expect(vm.stableVersions.length).toBe(1)

    // Click Delete
    const dependencyOperation = wrapper.find('.dependency-operation')

    await dependencyOperation.trigger('click')

    // Emit to update external data
    expect(wrapper.emitted().deletePackage).toBeTruthy()
  })
})

function getMouseEnterWrapper() {
  const pkg = {
    key: 'platform'.appId: 60531.name: 'You're so handsome.'.version: 'latest'.currentVersion: null.versions: [{ver: '3.0.5.24'.state: 5
      },
      {
        ver: '3.0.5.23'.state: 5
      },
      {
        ver: '3.0.5.22'.state: 5}}]const wrapper = mount(Dependency, {
    propsData: {
      pkg,
      appType: 0
    },
    localVue
  })

  return wrapper
}

Copy the code
Running results of business components

Test the interface request case

List.vue

<template>
  <div class="list-wrapper">
    <div class="list-item" v-for="item in listData">
      <span class="list-name">{{ item.name }}</span>
      <span class="list-img">{{ item.img }}</span>
      <span class="list-price">{{ item.price }}</span>
    </div>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  name: 'list'.data() {
    return {
      listData: []}},created() {
    this.getList()
  },
  methods: {
    async getList() {
      const response = await axios.get('mock/service')
      this.listData = response.data
    }
  }
}
</script>

<style></style>
Copy the code

List.spec.js

import { mount } from '@vue/test-utils' import List from '@/components/List' import flushPromises from 'flush-promises' Const mockData = {'mock/service': [{name: 'songm', price: '10000', img: 'http://aa.png' }] } jest.mock('axios', () => ({ get: jest.fn(url => Promise.resolve({ data: MockData [url]}})))) go (' List ', () = > {it (' simulation request: ', async () => { const wrapper = mount(List) await flushPromises() expect(wrapper.vm.listData.length).toBe(1) expect(wrapper.findAll('.list-item').length).toBe(1) expect( wrapper .findAll('.list-name') .at(0) .text() ).toBe('songm') }) afterEach(() => { jest.clearAllMocks() }) })Copy the code

Here we use flush-Promises to wait for the Promise state to refresh and the Jest Mock interface to return mock data based on interface parameters

Note: Importing the AXIos interface from an external file also applies, and the above test case does not require any changes

api/test.js

import axios from 'axios'
export const getList = () = > axios.get('mock/service')
Copy the code

List.vue

<template>
  <div class="list-wrapper">
    <div class="list-item" v-for="item in listData">
      <span class="list-name">{{ item.name }}</span>
      <span class="list-img">{{ item.img }}</span>
      <span class="list-price">{{ item.price }}</span>
    </div>
  </div>
</template>

<script>
import { getList } from '@/api/test'
export default {
  name: 'list'.data() {
    return {
      listData: []}},created() {
    this.getList()
  },
  methods: {
    async getList() {
      const response = await getList()
      this.listData = response.data
    }
  }
}
</script>

<style></style>
Copy the code
Test the case of Vuex

TestVuex.vue

<template>
  <button @click="handleIncrement">{{ count }}</button>
</template>

<script>
import { mapActions, mapState } from 'vuex'
export default {
  name: 'test-vuex'.computed: {
    ...mapState(['count'])},methods: {
    ...mapActions(['changeCount']),
    handleIncrement() {
      const count = this.count + 1
      this.changeCount(count)
    }
  }
}
</script>

<style></style>
Copy the code

TestVue.spec.js

import { mount, createLocalVue } from '@vue/test-utils'
import TestAction from '@/components/TestAction'
import Vuex from 'vuex'
import store from '@/store'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('TestAction'.() = > {
  it('Click button to trigger changeCount call'.async() = > {const wrapper = mount(TestAction, { store, localVue })

    await wrapper.find('button').trigger('click')

    expect(wrapper.vm.count).toBe(1)})})Copy the code

When we test Vuex, we don’t care what the action does, or what the Store is, we care when the action fires and what the expected value is. We can test our Store module directly, or we can simulate the Store module. Of course, if we don’t care about the interface, Instead of testing them through Vue Test Utils and Vuex, you can Test them as JS modules

The case of testing vue-Router
import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import router from '@/router'
import App from '@/App.vue'

const localVue = createLocalVue()

localVue.use(VueRouter)

describe('App'.() = > {
  it('test the router'.async() = > {const wrapper = shallowMount(App, {
      localVue,
      router
    })
    expect(wrapper.vm.$route.path).toBe('/')
    await router.push('/about')
    expect(wrapper.vm.$route.path).toBe('/about')})})Copy the code

When testing the Router or Vuex, it is best to use localVue to install it to avoid unnecessary trouble. After we install the Vuie-Router, router-link and router-View components are registered. This means we no longer need to import them and can use them anywhere in the application.

conclusion

Overall, the benefits of unit testing are many, we also talked about in front, but strong wine is good, do not drink oh. In our daily work, we still need to give priority to functions and take single test as a supplement, so we can’t reverse priorities and put the cart before the horse, and finally spend a lot of time to maintain two sets of logic, so we try to cover the core components and core scenes in the project. Other obstacles we write unit tests is usually not capacity problem, but a matter of time, the fast pace of today’s Internet giant, usually don’t have enough time to write a single measurement, so how can you deliver a satisfactory answer sheet test, skill is very important, grasp the scale, proficient skills, clear pattern, I’m sure you can.

reference

Vue Test Utils official document

Testing Vue.js Applications —- Edd Yerburgh

Jest official documentation