Regarding unit testing, the most common question should be "Is front-end unit testing necessary?" Through this article, you will understand the necessity of unit testing and how to fully and reliably test the components we wrote in the Vue project .
This article was first published on the public account
Front-end ReadingFor more exciting content, please pay attention to the latest news on the official account.
The need for unit testing
Generally, in our impression, unit testing is the work of test engineers, and the front-end is responsible for the code; Baidu searches for Vue unit testing, and all the associated words are
And unit testing can also help us save a large part of the cost of self-testing. If we have an order display component, we will display the corresponding copy based on the order status and other business logic; we want to view the copy on the page If the display is correct, you need to fill in the order information before you can view it; if you add some new logical judgments the next day (the order you placed the day before has expired), then you have three choices , The first option is to fill in the order and pay again cumbersomely (and provide financial support to the boss), the second option is to ask the back-end colleague to change the order status for you (the back-end colleague will give you a blank eye to yourself) Experience), the third option is to proxy the interface or use mock data (you need to compile the entire project and run it for testing).
At this time, unit testing provides a fourth lower-cost test method. Write a test case to test our components and determine whether the copy is displayed in the way we expect; this method does not require dependency. The assistance of the end does not require any changes to the project, which can be said to save time and effort.
Test framework and assertion library
Speaking of unit testing, let's first introduce popular testing frameworks, mainly mocha and jest. Let me briefly introduce mocha, which is translated into Chinese
Compared with jest, the main difference between the two is that jest has a built-in assertion library with a relatively high level of integration.
//src/index.js
function addNum ( a, b ) {
return a + b;
}
module .exports = addNum;
copy the code
Then we can write our test files. All test files are placed in the test directory. Generally, the test file and the source file to be tested will have the same name to facilitate the correspondence. When running mocha, all js files in the test directory will be automatically changed. carry out testing:
//test/index.test.js
var addNum = require ( "../src/index" );
describe( "Test addNum function" , () => {
it( "The result of adding two numbers is the sum of two numbers" , () => {
if (addNum( 1 , 2 ) !== 3 ) {
throw new Error ( "The result of adding two numbers is not two numbers " );
}
});
});
Copy code
The above code is the syntax of the test script, a test script will include one or more
and
The result of the operation is passed, which is the result we want, indicating that our function is correct; but every time we judge by throwing an exception, it is somewhat tedious, and the assertion library appears; the purpose of the assertion is to run the test code Then compare with our expectations. If it is consistent with expectations, it means that there is no problem with the code; if it is inconsistent with expectations, there is a problem with the code; each test case will have an assertion at the end to judge, if there is no assertion, the test is meaningless Up.
As mentioned above, mocha generally matches the Chai assertion library, and chai has several assertion styles. The more common styles are should and expect. Let s look at these two assertions separately:
var chai = require ( "chai" ),
expect = chai.expect,
should = chai.should();
describe( "Test addNum function" , () => {
it( "1+2" , () => {
addNum( 1 , 2 ).should.equal( 3 );
});
it( "2+3" , () => {
expect(addNum( 2 , 3 )).to.be.equal( 5 );
});
});
Copy code
Here should is the post-position, after the variable is asserted, and the expect is the pre-position. As the beginning of the assertion, the two styles are purely a matter of personal preference; we find that here expect is a function obtained from chai, while should is direct Call, this is because should actually expands all objects
Unlike chai's multiple assertion styles, jest has a built-in assertion library expect, and its syntax is somewhat different:
describe( "Test addNum function" , () => {
it( "1+2" , () => {
expect(addNum( 1 , 2 )).toBe( 3 );
});
it( "2+3" , () => {
expect(addNum( 2 , 3 )).toBe( 5 );
});
});
Copy code
The expect in jest passes directly
Jest
Jest is a testing framework produced by Facebook. Compared with other testing frameworks, the biggest feature is that it has built-in commonly used testing tools, such as its own assertion and test coverage tools, which can be used out of the box. This is also the same as its official slogan matches.
Jest is a delightful JavaScript testing framework that focuses on
Concise.
Jest is almost zero configuration, it will automatically identify some commonly used test files, such as
{
"scripts" : {
"test" : "jest"
}
}
Copy code
When we run
test( "1+2" , () => {
expect(addNum( 1 , 2 )).toBe( 3 );
});
Copy code
Same as it function, test function also represents a test case, mocha only supports
Matcher
We often need to perform a matching test on the value returned by the test code. The above code
test( "test tobe" , () => {
expect( 2 + 2 ).toBe( 4 );
expect( true ).toBe( true );
const val = "team" ;
expect(val).toBe( "team" );
expect( undefined ).toBe( undefined );
expect( null ).toBe( null );
});
Copy code
The toBe function is used internally
test( "expect a object" , () => {
var obj = {
a : "1" ,
};
obj.b = "2" ;
expect(obj).toEqual({ a : "1" , b : "2" });
});
test( "expect array" , () => {
var list = [];
list.push( 1 );
list.push( 2 );
expect(list).toEqual([ 1 , 2 ]);
});
Copy code
We sometimes need to match undefined, null and other types or the true and false of expressions in conditional statements. Jest also has five functions to help us:
- toBeNull: only match null
- toBeUndefined: only match undefined
- toBeDefined: Contrary to toBeUndefined, it is equivalent to .not.toBeUndefined
- toBeTruthy: match any if statement is true
- toBeFalsy: match any if statement is false
test( "null" , () => {
const n = null ;
expect(n).toBeNull();
expect(n).not.toBeUndefined();
expect(n).toBeDefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
test( "0" , () => {
const z = 0 ;
expect(z).not.toBeNull();
expect(z).not.toBeUndefined();
expect(z).toBeDefined();
expect(z).not.toBeTruthy();
expect(z).toBeFalsy();
});
test( "undefined" , () => {
const a = undefined ;
expect(a).not.toBeNull();
expect(a).toBeUndefined();
expect(a).not.toBeDefined();
expect(a).not.toBeTruthy();
expect(a).toBeFalsy();
});
Copy code
toBeTruthy and toBeFalsy are used to determine whether the expression in the if statement is true, equivalent to `if(n)
For numerical data, we can sometimes judge by greater than or less than:
test( "number" , () => {
const val = 2 + 2 ;
//greater than
expect(val).toBeGreaterThan( 3 );
//greater than or equal to
expect(val).toBeGreaterThanOrEqual( 3.5 );
//less than
expect( val).toBeLessThan( 5 );
//less than or equal to
expect(val).toBeLessThanOrEqual( 4.5 );
//fully judge
expect(val).toBe( 4 );
expect(val).toEqual( 4 );
});
Copy code
Although we can also use toBe and toEqual to compare floating-point data, if we encounter some special floating-point data calculations, such as 0.1+0.2, there will be problems. We can pass
test( "float" , () => {
//expect(0.1 + 0.2).toBe(0.3); error report
expect( 0.1 + 0.2 ).toBeCloseTo( 0.3 );
});
Copy code
For data of iterable types such as arrays, sets, or strings, you can pass
Test ( "Expect Iterable" , () => {
const ShoppingList = [
"Diapers" ,
"Kleenex" ,
"Trash Bags" ,
"Paper Towels" ,
"Milk" ,
];
expect(shoppingList).toContain( "milk" );
expect( new Set (shoppingList)).toContain( " diapers " );
expect( "abcdef" ).toContain( "cde" );
});
Copy code
Asynchronous code
Asynchronous code is often involved in our projects. For example, setTimeout, interface requests, etc. will involve asynchrony, so how can these asynchronous codes be tested? Suppose we have a function that fetches data asynchronously
export function fetchData ( cb ) {
setTimeout ( () => {
cb( "res data" );
}, 2000 );
}
Copy code
After 2 seconds, a string is returned through the callback function. We can use one in the function of the test case
test( "callback" , ( done ) => {
function cb ( data ) {
try {
expect(data).toBe( "res data" );
done();
} catch (error) {
done();
}
}
fetchData(cb);
});
Copy code
We pass a callback function to fetchData, and make an assertion on the returned data in the callback function. After the assertion ends, we need to call done; if done is not called at the end, then Jest does not know when it will end, and an error will be reported; in our daily code , Will get data through promises, and transfer our
export function fetchData () {
return new Promise ( ( resolve, reject ) => {
setTimeout ( () => {
resolve( "promise data" );
}, 2000 );
});
}
Copy code
Jest supports returning a promise directly in the test case, and we can make an assertion in the then:
test( "promise callback" , () => {
return fetchData().then( ( res ) => {
expect(res).toBe( "promise data" );
});
});
Copy code
In addition to returning fetchData directly, we can also use it in the assertion
test( "promise callback" , () => {
return expect(fetchData()).resolves.toBe( "promise data" );
});
Copy code
In addition, Jest also supports
test( "async/await callback" , async () => {
const data = await fetchData();
expect(data).toBe( "promise data" );
});
Copy code
Global mount and unmount
Global mounting and unmounting are somewhat similar to Vue-Router's global guards, which do some operations before and after each navigation is triggered; there are also in Jest, for example, we need to initialize some data before each test case, or in each test Clear the data after the use case, you can use
let cityList = []
beforeEach( () => {
initializeCityDatabase();
});
afterEach( () => {
clearCityDatabase();
});
test( "city data has suzhou" , () => {
expect(cityList).toContain( "suzhou" )
})
test( "city data has shanghai" , () => {
expect(cityList).toContain( "suzhou" )
})
Copy code
In this way, init will be called before each test case is tested, and clear will be called after each end; we may
let cityList = []
beforeEach( () => {
return initializeCityDatabase().then( ( res )=> {
cityList = res.data
});
});
//Or use async/await
beforeEach( async () => {
cityList = await initializeCityDatabase();
});
Copy code
with
beforeEach( () => {
//apply to all tests
});
describe( "put test together" , () => {
beforeEach( () => {
//Only apply the test in the current describe block
});
test( "test1" , ()=> {})
test( "test2" , ()=> {})
});
Copy code
Simulation function
In a project, a function of a module often calls a function of another module. In unit testing, we may not need to care about the execution process and results of internally called functions, but only want to know whether the function of the called module is called correctly, and even specify the return value of the function, so it is necessary to simulate the function.
If we are testing a function forEach, its parameters include a callback function that acts on each element of the array:
export function forEach ( items, callback ) {
for ( let index = 0 ; index <items.length; index++) {
callback(items[index]);
}
}
Copy code
In order to test this forEach, we need to build a simulation function to check whether the simulation function is called as expected:
test( "mock callback" , () => {
const mockCallback = jest.fn( ( x ) => 42 + x);
forEach([ 0 , 1 , 2 ], mockCallback);
expect(mockCallback.mock.calls.length).toBe( 3 );
expect(mockCallback.mock.calls[ 0 ][ 0 ]).toBe( 0 );
expect(mockCallback.mock.calls[ 1 ][ 0 ]).toBe( 1 );
expect(mockCallback.mock.calls[ 2 ][ 0 ]).toBe( 1 );
expect(mockCallback.mock.results[ 0 ].value).toBe( 42 );
});
Copy code
We found that there is a special
It has four attributes:
- calls: call parameters
- instances: this points to
- invocationCallOrder: function call order
- results: call result
There is one in the above attributes
test( "mock callback" , () => {
const mockCallback = jest.fn( ( x ) => 42 + x);
const obj = { a : 1 };
const bindMockCallback = mockCallback.bind(obj);
forEach([ 0 , 1 , 2 ], bindMockCallback);
expect(mockCallback.mock.instances[ 0 ]).toEqual(obj);
expect(mockCallback.mock.instances[ 1 ]).toEqual(obj);
expect(mockCallback.mock.instances[ 2 ]).toEqual(obj);
});
Copy code
After changing the this of the function through bind, we can use
const myMock = jest.fn();
//undefined
console .log(myMock());
myMock
.mockReturnValueOnce( 10 )
.mockReturnValueOnce( "x" )
.mockReturnValue( true );
//10 x true true
console .log(myMock(), myMock(), myMock(), myMock());
myMock.mockReturnValueOnce( null );
Null to true to true//
Console .log (myMock (), myMock (), myMock ());
duplicated code
We execute myMock for the first time, since no return value is injected, and then pass
const filterFn = jest.fn();
filterFn.mockReturnValueOnce( true ).mockReturnValueOnce( false );
const result = [ 2 , 3 ].filter( ( num ) => filterFn(num));
expect(result).toEqual([ 2 ]);
Copy code
We can also make an assertion on the call of the simulated function:
const mockFunc = jest.fn();
//Assert that the function has not been called
expect(mockFunc).not.toHaveBeenCalled();
mockFunc( 1 , 2 );
mockFunc( 2 , 3 );
//Assert that the function is called at least once
expect(mockFunc).toHaveBeenCalled();
//Assert function call parameters
expect(mockFunc).toHaveBeenCalledWith( 1 , 2 );
the Expect (mockFunc) .toHaveBeenCalledWith ( 2 , 3 );
the last parameter of the function call//assert
the Expect (mockFunc) .toHaveBeenLastCalledWith ( 2 , 3 );
Copy the code
In addition to simulating functions, Jest also supports intercepting axios return data. If we have an interface to obtain users:
///src/api/users
const axios = require ( "axios" );
function fetchUserData () {
return axios
.get( "/user.json" )
.then( ( resp ) => resp.data);
}
module .exports = {
fetchUserData,
};
Copy code
Now we want to test
const users = require("../api/users");
const axios = require("axios");
jest.mock("axios");
test("should fetch users", () => {
const userData = {
name: "aaa",
age: 10,
};
const resp = { data: userData };
axios.get.mockResolvedValue(resp);
return users.fetchUserData().then((res) => {
expect(res).toEqual(userData);
});
});
Once we simulate the module, we can use the get function to provide a mockResolvedValue method to return the data we need to test; after the simulation, axios does not actually send a request to get it.
Vue Test Utils
Vue Test Utils is the official unit test utility library of Vue.js, which can test the Vue components we write.
Mount components
In Vue we pass
<!-- Counter.vue -->
<template>
<div class="counter">
<span class="count">{{ count }}</span>
<button id="add" @click="add"> </button>
</div>
</template>
<script>
export default {
data() {
return {
count : 0 ,
};
},
methods : {
add () {
this .count++;
},
},
};
</script >
copy code
After the component is mounted, a wrapper is obtained. The wrapper exposes many convenient methods for encapsulating, traversing, and querying its internal Vue component instances.
Import {} Mount from "@ VUE/Test-utils" ;
Import Counter from "@/Components/Counter" ;
const warpper = Mount (Counter);
const VM = wrapper.vm;
duplicated code
We can pass
//test/unit/counter.spec.js
describe("Counter", () => {
const wrapper = mount(Counter);
test("counter class", () => {
expect(wrapper.classes()).toContain("counter");
expect(wrapper.classes("counter")).toBe(true);
});
test("counter has span", () => {
expect(wrapper.html()).toContain("<span class="count">0</span>");
});
test( "counter has btn" , () => {
expect(wrapper.find( "button#add" ).exists()).toBe( true );
expect(wrapper.find( "button#add" ).exists()).not.toBe( false );
});
});
Copy code
We can guess what the above functions do based on their names:
- classes: Get the wrapper class and return an array
- html: Get component rendering html structure string
- find: Returns the wrapper that matches the child element
- exists: Assert whether the wrapper exists
What find returns is the first DOM node found, but in some cases we want to be able to manipulate a set of DOM, we can use
const wrapper = mount(Counter);
//return a set of wrappers
const divList = wrapper.findAll( 'div' );
divList.length
//find the first div, it returns warpper
const firstDiv = divList.at ( 0 );
duplicated code
props slots provide/inject mount
const wrapper = mount(Component, {
// data data
data() {
return {
foo: "bar"
}
},
// props
propsData: {
msg: "hello"
},
//vue
localVue,
//
mocks: {
$route
},
//
// slot
//
slots: {
default: SlotComponent,
foo: "<div/>",
bar: "<my-component/>",
baz: ""
},
//Used to register custom component
stubs : {
"my-component" : MyComponent,
"el-button" : true ,
},
//Set the $attrs object of the component instance.
attrs : {},
//Set the $listeners object of the component instance.
listeners : {
click: jest.fn()
},
//Pass the attribute for injection to the component
provide : {
foo() {
return "fooValue"
}
}
})
Copy code
When we unit test a component, we hope to test only a single component to avoid the side effects of sub-components; for example, we are
import { shallowMount } from '@vue/test-utils'
const wrapper = shallowMount(Component)
data Form
<template>
<div class="form">
<div class="title">{{ title }}</div>
<div>
<span> </span>
<input type="text" id="name-input" v-model="name"/>
<div class="name">{{ name }}</div>
</div>
<div>
<span> </span>
<input type="radio" name="sex" v-model="sex" value="f" id=""/>
<input type="radio" name="sex" v-model="sex" value="m" id=""/>
</div>
<div>
<span> </span>
footbal
<input
type="checkbox"
name="hobby"
v-model="hobby"
value="footbal"
/>
basketball
<input
type="checkbox"
name="hobby"
v-model="hobby"
value="basketball"
/>
ski
<input type="checkbox" name="hobby" v-model="hobby" value="ski"/>
</div>
<div>
<input
:class="submit ? 'submit' : ''"
type="submit"
value=" "
@click="clickSubmit"
/>
</div>
</div>
</template>
<script>
export default {
name: "Form",
props: {
title: {
type: String,
default: " ",
},
},
data() {
return {
name: "",
sex: "f",
hobby: [],
submit: false,
};
},
methods: {
clickSubmit() {
this.submit = !this.submit;
},
},
};
</script>
Form title input radio checkbox props
const wrapper = mount(Form, {
propsData: {
title: "form title",
},
});
const vm = wrapper.vm;
test("change prop", () => {
expect(wrapper.find(".title").text()).toBe("form title");
wrapper.setProps({
title: "new form title",
});
//
expect(wrapper.find(".title").text()).toBe("new form title");
});
Vue prop data dom
test("change prop1", async () => {
expect(wrapper.find(".title").text()).toBe("new form title");
wrapper.setProps({
title: "new form title1",
});
await Vue.nextTick();
// vm nextTick
//await wrapper.vm.nextTick();
expect(wrapper.find(".title").text()).toBe("new form title1");
});
test("change prop2", (done) => {
expect(wrapper.find(".title").text()).toBe("new form title1");
wrapper.setProps({
title: "new form title2",
});
Vue.nextTick(() => {
expect(wrapper.find(".title").text()).toBe("new form title2");
done();
});
});
Jest
test("test set data", async () => {
wrapper.setData({
name: "new name",
});
expect(vm.name).toBe("new name");
await Vue.nextTick();
expect(wrapper.find(".name").text()).toBe("new name");
});
input textarea select
test("test input set value", async () => {
const input = wrapper.find("#name-input");
await input.setValue("change input by setValue");
expect(vm.name).toBe("change input by setValue");
expect(input.element.value).toBe("change input by setValue");
});
//
test("test input trigger", () => {
const input = wrapper.find("#name-input");
input.element.value = "change input by trigger";
// input.element.value trigger
input.trigger("input");
expect(vm.name).toBe("change input by trigger");
});
radio checkbox
test("test radio", () => {
expect(vm.sex).toBe("f");
const radioList = wrapper.findAll('input[name="sex"]');
radioList.at(1).setChecked();
expect(vm.sex).toBe("m");
});
test("test checkbox", () => {
expect(vm.hobby).toEqual([]);
const checkboxList = wrapper.findAll('input[name="hobby"]');
checkboxList.at(0).setChecked();
expect(vm.hobby).toEqual(["footbal"]);
checkboxList.at(1).setChecked();
expect(vm.hobby).toEqual(["footbal", "basketball"]);
checkboxList.at(0).setChecked(false);
expect(vm.hobby).toEqual(["basketball"]);
});
test("test click", async () => {
const submitBtn = wrapper.find('input[type="submit"]');
await submitBtn.trigger("click");
expect(vm.submit).toBe(true);
await submitBtn.trigger("click");
expect(vm.submit).toBe(false);
});
{
methods: {
clickSubmit() {
this.$emit("foo", "foo1", "foo2");
this.$emit("bar", "bar1");
},
},
}
$emi
wrapper.vm.$emit("foo", "foo3");
$emit
{
foo: [ [ 'foo1', 'foo2' ], [ 'foo3' ] ],
bar: [ [ 'bar1' ] ]
}
test("test emit", async () => {
// emit
await wrapper.find('input[type="submit"]').trigger("click");
wrapper.vm.$emit("foo", "foo3");
await vm.$nextTick();
//foo
expect(wrapper.emitted().foo).toBeTruthy();
//foo
expect(wrapper.emitted().foo.length).toBe(2);
// foo
expect(wrapper.emitted().foo[0]).toEqual(["foo1", "foo2"]);
//baz
expect(wrapper.emitted().baz).toBeFalsy();
});
expect(wrapper.emitted('foo')).toBeTruthy();
expect(wrapper.emitted('foo').length).toBe(2);
emit vm emit
import { mount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent'
import ChildComponent from '@/components/ChildComponent'
describe('ParentComponent', () => {
it("emit", () => {
const wrapper = mount(ParentComponent)
wrapper.find(ChildComponent).vm.$emit('custom')
})
})
Vue-Router
<template>
<div>
<div @click="jump">{{ $route.params.id }}</div>
<router-link :to="{ path: '/detail' }"></router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
data() {
return {};
},
mounted() {},
methods: {
jump() {
this.$router.push({
path: "/list",
});
},
},
};
</script>
import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import Header from "@/components/Header";
// Vue
const localVue = createLocalVue()
localVue.use(VueRouter)
//
const routes = []
const router = new VueRouter({
routes
})
shallowMount(Header, {
localVue,
router
})
import { mount } from "@vue/test-utils";
import Header from "@/components/Header";
describe("header", () => {
const $route = {
path: "/home",
params: {
id: "111",
},
};
const $router = {
push: jest.fn(),
};
const wrapper = mount(Header, {
stubs: ["router-view", "router-link"],
mocks: {
$route,
$router,
},
});
const vm = wrapper.vm;
test("render home div", () => {
expect(wrapper.find("div").text()).toBe("111");
});
});
$route
Vuex
vuex
<template>
<div>
<div class="number">{{ number }}</div>
<div class="add" @click="clickAdd">add</div>
<div class="sub" @click="clickSub">sub</div>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
export default {
name: "Count",
computed: {
...mapState({
number: (state) => state.number,
}),
},
methods: {
clickAdd() {
this.$store.commit("ADD_COUNT");
},
clickSub() {
this.$store.commit("SUB_COUNT");
},
},
};
</script>
vuex
export default new Vuex.Store({
state: {
number: 0,
},
mutations: {
ADD_COUNT(state) {
state.number = state.number + 1;
},
SUB_COUNT(state) {
state.number = state.number - 1;
},
}
});
import { mount, createLocalVue } from "@vue/test-utils";
import Count from "@/components/Count";
import Vuex from "vuex";
const localVue = createLocalVue();
localVue.use(Vuex);
describe("count", () => {
const state = {
number: 0,
};
const mutations = {
ADD_COUNT: jest.fn(),
SUB_COUNT: jest.fn(),
};
const store = new Vuex.Store({
state,
mutations
});
test("render", async () => {
const wrapper = mount(Count, {
store,
localVue,
});
expect(wrapper.find(".number").text()).toBe("0");
wrapper.find(".add").trigger("click");
expect(mutations.ADD_COUNT).toHaveBeenCalled();
expect(mutations.SUB_COUNT).not.toHaveBeenCalled();
});
});
VueRouter
export default {
state: {
list: [],
},
getters: {
joinList: (state) => {
return state.list.join(",");
},
},
mutations: {
PUSH(state, payload) {
state.list.push(payload);
},
},
};
import { createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
import { cloneDeep } from "lodash";
import listStore from "@/store/list";
describe("list", () => {
test("expect list", () => {
const localVue = createLocalVue();
localVue.use(Vuex);
const store = new Vuex.Store(cloneDeep(listStore));
expect(store.state.list).toEqual([]);
store.commit("PUSH", "1");
expect(store.state.list).toEqual(["1"]);
});
test("list getter", () => {
const localVue = createLocalVue();
localVue.use(Vuex);
const store = new Vuex.Store(cloneDeep(listStore));
expect(store.getters.joinList).toBe("");
store.commit("PUSH", "1");
store.commit("PUSH", "3");
expect(store.getters.joinList).toBe("1,3");
});
});
store store commit getters