The Vue.js project uses Karma to automate UI testing

Internal test code should not be disclosed, so take the test code of the open source project Rubik UI as an example

Just a summary of the stage, there is no comprehensive

The environment

After installing Karma and automatically generating Karma.conf.js

Mocha was chosen as the testing framework

Chai as assertion library (add Sino-chai for extension)

Use Chrome and Phantom. Js

The test code is also packaged in karma-webpack

Finally, spec and karma-coverage are used to report test coverage

Therefore, a bunch of plugins need to be installed…

configuration

karma.conf

Add files Preprocessors webpack plugins to the configuration file karmap.conf.js that karma automatically generates

The complete karmap.conf.js file

Files and preprocessors

According to the official example provided by vue.js

In karmap.conf.js files is configured as files: [‘./file.js’],

The core code in file.js is as follows:

Const testsContext = require.context('./specs', true, /\.spec$/) testsContext.keys().forEach(testsContext)Copy the code

The require.context of webpack is used here to import all the.spec.js files under the specs subdirectory

The configuration item for Preprocessors is

preprocessors: {
  './file.js': ['webpack', 'sourcemap', 'coverage']
},Copy the code

All files require in file.js need webpack

The complete file.js file

webpack

const webpackConfig = require('.. /.. /build/webpack.test.conf'); {... webpack: webpackConfig, ... }Copy the code

The webpack.test.conf.js file introduced above is no different from the normal Webpack configuration.

Here for the sake of convenience, directly copy the webpack.base.conf.js file, again based on the modification

Webpack. Test. Conf. Js file

plugins

This one is easy. Include all the plug-ins you need

{... plugins: [ 'karma-webpack', 'karma-sourcemap-loader', 'karma-mocha', 'karma-chai', 'karma-sinon-chai', 'karma-chrome-launcher', 'karma-phantomjs-launcher', 'karma-spec-reporter', 'karma-coverage' ], ... }Copy the code

Importing component libraries

Because the introduction of Rubik UI requires two steps

  1. use: Vue.use(Rubik)
  2. In the hook functionmountedTo initialize:vm.$rubik.init()

Since the nature of the init function is to add a click or touchstar event to the body element, the mounted hook can be ignored in the test as long as the body element is present

The file.js file in the configuration above is also packed by Webpack and loaded in the browser, so the initialization of the Rubik UI is placed in file.js

Import Vue from 'Vue' import {createVM} from './vm' import Rubik from 'SRC /index.js' vue. use(Rubik) // VM is Vue Const vm = createVM({}, true) vm.$rubik.init() const vm = createVM({}, true) vm.Copy the code

CreateVM and vm.js are described in more detail below

Vue Unit Test

Unit Test is officially documented

We made a simple change here by referring directly to a utility function in the component library Element UI

In the vm.js file we can create several utility functions to generate instances of Vue

createComponent

// Create an instance of exports.createComponent = function(Component, props = {}, mounted = false) { if (props === true || props === false) { mounted = props props = {} } const elm = createElm() // Const Ctor = vue.extend (Component) return new Ctor({props}).$mount(mounted === false? null : elm) }Copy the code

Since many components are single-file components of.vue, you can use createComponent to generate the corresponding instance to mount into the DOM during testing

createVM

exports.createVM = function(Component, Mounted = false) {const elm = createElm () / / located on the body to create an empty div if (Object. The prototype. ToString. Call = = = (Component) '[object String]') { Component = { template: Component } } return new Vue(Component).$mount(mounted === false ? null : elm) }Copy the code

Much like createComponent, templates can be passed directly to generate more complex instance objects, such as

createVM({ template: '<div> < r-bTN class="green white-text" V-dropdown :dropdown> dropdown menu </ r-bTN >< r-dropdown ID ="dropdown" right hover> <li><a Class = "dropdown can - item" href = "#" > eat < / a > < / li > < li > < a class = "dropdown can - item" href = "#" > sleep < / a > < / li > < / r - dropdown can > < / div > `}, true)Copy the code

CreateComponent and createVM have the flexibility to generate vue instances of single-file components and custom tag components, and then simulate various operations to perform unit tests

Go back to the questions left in the introduction component library section above

// vm is an instance of Vue const vm = createVM({}, true) vm.$rubik.init()Copy the code

After createVM generates an empty Vue instance object VM, the VM can directly call the init method to register events.

The init function is already attached to the prototype chain of Vue

Vue.prototype.$rubik = {
    ...
    init: Init,
    ...
  }Copy the code

Each individual Vue instance object makes it easy to call the init function

test

Basically writing test cases in *.spec.js

This is very much about components and business, and there are two things to note

  1. Because Vue uses the DOM mechanism of asynchronous updates, some assertions that depend on the results of DOM updates need to be made inVue.nextTickIs executed in a callback
  2. Assertions that emulate mouse and keyboard events are also executed after an event loop, which can be simplesetTimeout( _ => { }, interval)Interval can be adjusted based on the execution time of the element animation

The code to simulate mouse and keyboard actions is also placed in the vm.js file

// Trigger events: mouseEnter, mouseleave, mouseover, keyup, change, click... exports.fireEvent = function(elm, name, ... opts) { let eventName; if (/^mouse|click/.test(name)) { eventName = 'MouseEvents' } else if (/^key/.test(name)) { eventName = 'KeyboardEvent' }  else { eventName = 'HTMLEvents' } const evt = document.createEvent(eventName) evt.initEvent(name, ... opts) elm.dispatchEvent ? elm.dispatchEvent(evt) : elm.fireEvent('on' + name, evt) return elm }Copy the code

Take one of raido’s test cases

. fireEvent(radio2, 'click') setTimeout( _ => { expect(vm.$data.color).to.be.equal('white') expect(radio1.checked).to.be.false expect(radio2.checked).to.be.true done() }, 100) ...Copy the code

Execute the assertion 100ms after you simulate Click

Radio.spec.js detailed code

The last

  • Test cases, on the other hand, are very good API documents. During my internship, my senior brother told me that if I could not understand the code, I should first read the unit tests, and then I would use THE API if I understood the test cases
  • Good code is easy to test. If the test is inefficient, then the production environment will be inefficient, easy to test will be easy to produce. So refactoring code after testing will be of higher quality than refactoring code directly
  • TDD is a very effective way of coding, because testing can quickly feedback our design ideas and validate ideas
  • (Feels like a lot of crap…)