The most common question about unit testing is “Is front-end unit testing necessary?” Through this article, you will understand the need for unit testing and how you can reliably test the components we write in a Vue project.

This article was first published in the public number [front-end one read], more exciting content please pay attention to the latest news of the public number.

The need for unit testing

In general, we are under the impression that unit testing is the job of the test engineer, the front-end is responsible for the code; Baidu search Vue unit test, the association of words are “is the unit test necessary?” “What do unit tests do?” Although we usually projects generally have test engineer to test our page “out”, but according to my observation, the general test engineer will not cover all the business logic, and some deep code logic test engineer in the case of don’t understand the code and it’s impossible to be triggered. Therefore, in this case, we can not completely rely on test engineers to test our project, the front-end project of the unit test is very necessary.

And unit testing can also help us to save a large part of the cost of self-testing, if we have an order display component, according to the different order status and some other business logic to carry out the corresponding copy display; We want to check whether the copywriting display is correct on the page, then we need to fill in the tedious order information before viewing; If the next day and added some new logical judgment (the day before your next single would have expired), then you have three options, the first option is trival again to fill in the order and pay out (and provide financial support for the boss), the second option is described as beg for backend colleagues give you change the order status (back-end colleagues to give you a dirty look myself feel). The third option is to proxy the interface or use mock data (you need to compile the entire project to run for testing).

At this point, unit testing provides a fourth, less costly way of testing, writing a test case to test our components and determine whether the text is presented the way we expect it to be; This approach saves time and effort by not relying on the assistance of the back end or requiring any changes to the project.

Test frameworks and assertion libraries

Speaking of unit testing, let’s start with a look at the popular testing frameworks, primarily Mocha and Jest. A quick introduction to Mocha, which translates to mocha in Chinese (renjia is a kind of coffee! Not matcha), the name is probably derived from the fact that developers like to drink mocha coffee. Just as Java got its name from coffee, the Mocha logo is also a cup of mocha coffee:

The main difference between jEST and JEST is that Jest has a highly integrated assertion library expect. Js, while Mocha needs additional assertion libraries, such as the popular CHAI library. Let’s take a look at how Mocha tests the code. First we write an addNum function, but we are not sure if it returns the result we want, so we need to test this function:

//src/index.js
function addNum(a, b) {
  return a + b;
}
module.exports = addNum;
Copy the code

Mocha will automatically test all js files in the “test” directory. If you run mocha, you will automatically test all js files in the “test” directory.

//test/index.test.js
var addNum = require(".. /src/index");
describe("Test addNum function".() = > {
  it("The sum of two numbers is the sum of two numbers.".() = > {
    if (addNum(1.2)! = =3) {
      throw new Error("The sum of two numbers is not the sum of two numbers."); }}); });Copy the code

The above code is the syntax for a test script that includes one or more describe blocks, each of which contains one or more IT blocks; Describe a set of related tests that take two parameters: the first parameter is the name of the test suite and the second parameter is the actual function executed.

It is called a test case, which represents a single test and is the smallest unit of testing. It also contains two parameters, the first parameter is the name of the test case and the second parameter is the actual function executed.

The IT block is the code we need to test and throw an exception if the result is not what we expect; With the above test cases written, we can run the tests,

The result of the run passes, which is what we want, which means our function is correct; But every time you throw an exception, it gets a little tedious, and you get the assertion library; The purpose of an assertion is to compare the test code to what we expect when it runs. If it doesn’t, there’s something wrong with the code; At the end of each test case there is an assertion to judge, without which the test is meaningless.

Mocha uses a library of chai assertions, and there are several types of chai assertions. The most common types of chai assertions are should and Expect.

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 the code

Here should comes after the assertion of variables, and expect comes before, as the beginning of the assertion. Both styles are purely personal; We see here that Expect gets a function from Chai, and that should is called directly, because should actually extends the getter property should to all objects, so that we can use the. Should mode for assertions on variables.

Unlike chai’s many assertion styles, Jest comes with the expect assertion library, which has a slightly different syntax:

describe("Test addNum function".() = > {
  it("1 + 2".() = > {
    expect(addNum(1.2)).toBe(3);
  });
  it("2 + 3".() = > {
    expect(addNum(2.3)).toBe(5);
  });
});
Copy the code

Expect in Jest is more concise in form than Mocha, thanks directly to toBe’s syntax; The two frameworks are very similar in usage. For example, both support done callbacks and async/await keywords in asynchronous code, and there are some differences in assertion syntax and other usage. Both also have the same hook mechanism, same name beforeEach and afterEach; You can also choose between two frameworks when creating a vue CLI scaffolding project, which we tested primarily with JEST.

Jest

Jest is a testing framework produced by Facebook. Compared with other testing frameworks, its biggest feature is that it has built-in common testing tools, such as built-in assertions and test coverage tools, which is implemented out of the box, which is also consistent with its official slogan.

Jest is an enjoyable JavaScript testing framework that focuses on simplicity.

Jest has almost zero configuration. It automatically recognizes common test files such as *.spec.js and *.test.js scripts, all of which are placed in the tests or __tests__ directory. We can install Jest globally or locally and specify the test script in packs.json:

{
  "scripts": {
    "test": "jest"}}Copy the code

When we run NPM run test, we will automatically run all test files in the test directory to complete the test. We may also see test cases written using the test function on the Jest website:

test("1 + 2".() = > {
  expect(addNum(1.2)).toBe(3);
});
Copy the code

Like the IT function, the test function also represents a test case. Mocha only supports IT, while Jest supports IT and test.

matcher

We often need to match the values returned by the test code, and toBe in the above code is the simplest matcher to test whether two values are the same.

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 the code

The toBe function internally uses Object.is for exact matching, which has properties like ===; It is possible to compare values of ordinary types, but for complex types such as arrays of objects, toEqual is used:

    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 the code

Jest also has five functions to help us:

  • ToBeNull: Matches only null
  • ToBeUndefined: matches only undefined
  • ToBeDefined: The opposite of toBeUndefined, equivalent to. Not. ToBeUndefined
  • ToBeTruthy: Matches any if statement as true
  • ToBeFalsy: Matches any if statement as 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 the code

ToBeTruthy and toBeFalsy are used to determine whether an expression in an if statement is true, equivalent to ‘if(n)’ and ‘if(! N) ‘s judgment.

For numeric data, we can sometimes judge by greater or less than:

test("number".() = > {
    const val = 2 + 2;
    / / is greater than the
    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);
    // Complete judgment
    expect(val).toBe(4);
    expect(val).toEqual(4);
});
Copy the code

ToBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo: toBeCloseTo

test("float".() = > {
    / / expect (0.1 + 0.2). The place (0.3); An error
    expect(0.1 + 0.2).toBeCloseTo(0.3);
});
Copy the code

If an array, set, or string contains an iterable item, you can use toContain to determine whether it contains an item:

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 the code

Asynchronous code

Asynchronous code is often involved in our projects, such as setTimeout, interface request, etc., which will be involved in asynchronous code, so how to test these asynchronous codes? Suppose we have a function fetchData that fetches data asynchronously:

export function fetchData(cb) {
  setTimeout(() = > {
    cb("res data");
  }, 2000);
}
Copy the code

After 2 seconds, the callback returns a string. We can use a done argument in the function of the test case. Jest will wait for the done callback before completing the test:

test("callback".(done) = > {
  function cb(data) {
    try {
      expect(data).toBe("res data");
      done();
    } catch (error) {
      done();
    }
  }
  fetchData(cb);
});
Copy the code

We pass a callback function to fetchData, assert the returned data in the callback function, and call done at the end of the assertion. If done is not called at the end, Jest does not know when to end, and an error is reported. In our daily code, we use promises to fetchData, and rewrite our fetchData:

export function fetchData() {
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      resolve("promise data");
    }, 2000);
  });
}
Copy the code

Jest supports returning a promise directly in test cases, which we can assert in THEN:

test("promise callback".() = > {
  return fetchData().then((res) = > {
    expect(res).toBe("promise data");
  });
});
Copy the code

Apart from returning fetchData directly, we can also use this in assertions.

test("promise callback".() = > {
  return expect(fetchData()).resolves.toBe("promise data");
});
Copy the code

Jest also supports async/await, but we need to add the async modifier to the anonymous function of test:

test("async/await callback".async() = > {const data = await fetchData();
  expect(data).toBe("promise data");
});
Copy the code

Global mount and unmount

Global mounting and unmounting is a bit like vue-Router’s global guard, doing something before and after each navigation trigger; In Jest, too, if we need to initialize some data beforeEach test case, or clear it afterEach test case, we can use beforeEach and afterEach:

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 the code

Thus, init is called before each test case and clear is called after each test case; It is possible to change the cityList data in some tests, but after initialization beforeEach, the cityList data is guaranteed to be the same for each test case; As with the asynchronous code above, we can use asynchronous code to initialize beforeEach and afterEach:

let cityList = []
beforeEach(() = > {
  return initializeCityDatabase().then((res) = >{
    cityList = res.data
  });
});
// Use async/await
beforeEach(async () => {
  cityList = await initializeCityDatabase();
});
Copy the code

BeforeAll and afterAll correspond to beforeEach and afterEach. The difference is that beforeAll and afterAll are executed only once. BeforeEach and afterEach are applied to each test by default, but we might want to target only certain tests. We can use describe to group these tests together so that only tests in the Describe block are applied:

beforeEach(() = > {
  // Apply to all tests
});
describe("put test together".() = > {
  beforeEach(() = > {
    // Apply only test in the current Describe block
  });
  test("test1".() = > {})
  test("test2".() = >{})});Copy the code

Simulation function

In a project, it is common for a function in one module to call a function in another module. In unit tests, we may not care about the execution process and results of internally called functions, but just 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 very necessary to simulate functions.

If we are testing a function forEach, its arguments include a callback function on each element of the array:

export function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) { callback(items[index]); }}Copy the code

To test this forEach, we need to build a mock function to check if the mock 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 the code

We noticed that there is a special. Mock attribute in mockCallback that holds information about the mock function being called; Let’s print it out:

It has four properties:

  • Calls: Call parameters
  • Instances: this point to
  • InvocationCallOrder: Function call order
  • Results: results of the call

We can also change the this of our simulated function by using the bind function:

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 the code

After changing this of the function by bind, we can use instances to check; A mock function can inject a return value at run time:

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 true true
console.log(myMock(), myMock(), myMock());
Copy the code

The first time we do myMock, since we don’t inject any return values, and then we do return value injection with mockReturnValueOnce and mockReturnValue, Once is injected only Once; It is useful to use injection when the continuity function passes a return value:

const filterFn = jest.fn();
filterFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
const result = [2.3].filter((num) = > filterFn(num));
expect(result).toEqual([2]);
Copy the code

We can also make assertions about mock function calls:

const mockFunc = jest.fn();

// The assertion function has not been called yet
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);
expect(mockFunc).toHaveBeenCalledWith(2.3);
// Assert the last call argument to the function
expect(mockFunc).toHaveBeenLastCalledWith(2.3);
Copy the code

In addition to being able to emulate functions, Jest also supports intercepting axios returns data if we have an interface to retrieve the user:

// /src/api/users
const axios = require("axios");

function fetchUserData() {
  return axios
    .get("/user.json")
    .then((resp) = > resp.data);
}

module.exports = {
  fetchUserData,
};
Copy the code

Now that we want to test the fetchUserData function to fetch data without actually requesting the interface, we can use jest. Mock to emulate the AXIos module:

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);
  });
});
Copy the code

Once we have simulated the module, we can provide a mockResolvedValue method with the GET function to return the data we need to test; After the simulation, Axios doesn’t actually send a request to fetch /user.json data.

Vue Test Utils

Vue Test Utils is the official Vue. Js unit Test utility library that tests Vue components we write.

Mount components

In Vue we import components and use them after registering components; In unit tests, we used mount to mount components; Suppose we write a counter component counter.js that displays count and has a button that operates on count:

<! -- Counter.vue -->
<template>
  <div class="counter">
    <span class="count">{{ count }}</span>
    <button id="add" @click="add">add</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      count: 0}; },methods: {
    add() {
      this.count++; ,}}};</script>
Copy the code

The component is mounted to get a Wrapper, which exposes many convenient ways to encapsulate, traverse, and query Vue component instances within it.

import { mount } from "@vue/test-utils";
import Counter from "@/components/Counter";
const wrapper = mount(Counter);
const vm = wrapper.vm;
Copy the code

We can access a Vue instance of a component via wrapper.vm to retrieve methods, data, etc. Using the Wrapper, we can assert the rendering of the component:

// 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 the code

The names of these functions can also be used to guess what they do:

  • Classes: Gets the Wrapper class and returns an array
  • HTML: Gets the component’s rendering HTML structure string
  • Find: Returns a wrapper that matches a child element
  • Exists: Asserts whether a wrapper exists

Find returns the first DOM node searched, but in some cases we want to manipulate a set of DOM nodes. We can use the findAll function:

const wrapper = mount(Counter);
// Return a set of wrappers
const divList = wrapper.findAll('div');
divList.length
// Find the first div and return its wrapper
const firstDiv = divList.at(0);
Copy the code

Some components need props, slots, provide/inject, and other plugins or properties passed in externally. We can pass in an object to set these additional properties when mounting:

const wrapper = mount(Component, {
  // Pass data to the component and merge it into the existing data
  data() {
    return {
      foo: "bar"}},// Set the props of the component
  propsData: {
    msg: "hello"
  },
  // Vue local copy
  localVue,
  // Forge global objects
  mocks: {
    $route
  },
  / / slots
  // The key name is the corresponding slot name
  // The key value can be a component, an array of components, a string template, or text.
  slots: {
    default: SlotComponent,
    foo: "<div />".bar: "<my-component />".baz: ""
  },
  // To register custom components
  stubs: {
    "my-component": MyComponent,
    "el-button": true,},// Set the component instance's $attrs object.
  attrs: {},
  // Sets the $Listeners object for the component instance.
  listeners: {
    click: jest.fn()
  },
  // Pass attributes to the component for injection
  provide: {
    foo() {
      return "fooValue"}}})Copy the code

Stubs is mainly used to handle globally registered custom components, such as our common component library Element, directly using el-button, el-input components, or vue-router registered in the global router-view components. When introduced in our unit tests, we are told that the corresponding component is not found, so we can use this stuBS to avoid errors.

When we unit test a component, we want to test only for a single component to avoid the side effects of sub-components. For example, when we determine whether a div exists in the ParentComponent ParentComponent, the ChildComponent renders that div. This will cause some interference in our testing. We can use the shallowMount mount function to meet more than mount, shallowMount does not render child components:

import { shallowMount } from '@vue/test-utils'
const wrapper = shallowMount(Component)
Copy the code

This ensures that the component we need to test will not render its children during rendering, avoiding interference from the children.

Operating components

We often need to operate and modify elements or data of sub-components, such as page click, modify data data, and then assert whether the data is correct after operation; Take a simple Form component as an example:

<template>
  <div class="form">
    <div class="title">{{ title }}</div>
    <div>
      <span>Please fill in your name:</span>
      <input type="text" id="name-input" v-model="name" />
      <div class="name">{{ name }}</div>
    </div>
    <div>
      <span>Please select gender:</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>Please select a hobby:</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="Submit"
        @click="clickSubmit"
      />
    </div>
  </div>
</template>
<script>
export default {
  name: "Form".props: {
    title: {
      type: String.default: "Form name",}},data() {
    return {
      name: "".sex: "f".hobby: [].submit: false}; },methods: {
    clickSubmit() {
      this.submit = !this.submit; ,}}};</script>
Copy the code

We can pass a title to the Form component, which is the name of the Form. It also contains input, radio, and checkbox elements. Let’s look at how to modify these elements. First we need to change the props value. When the component is initialized we pass in propsData. Later in the code we can change the props value by setProps:

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"});/ / an error
  expect(wrapper.find(".title").text()).toBe("new form title");
});
Copy the code

We tested it in anticipation, but found that the last assertion was wrong; This is because Vue updates data asynchronously. When we change prop and data, we fetch dom and find that the data does not update immediately. On the page we usually do this with $nextTick, but in unit testing we can also use nextTick in conjunction with DOM fetching:

test("change prop1".async () => {
  expect(wrapper.find(".title").text()).toBe("new form title");
  wrapper.setProps({
    title: "new form title1"});await Vue.nextTick();
  // Or use 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();
  });
});
Copy the code

Just like testing asynchronous code in Jest, we can use done callbacks or async/await to test asynchronously; In addition to setting props, setData can be used to change data in the wrapper:

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");
});
Copy the code

For input component elements such as input, Textarea, or SELECT, there are two ways to change their values:

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");
});
/ / equivalent to the
test("test input trigger".() = > {
  const input = wrapper.find("#name-input");
  input.element.value = "change input by trigger";
  // If the value is changed by input.element.value, trigger must be triggered
  input.trigger("input");
  expect(vm.name).toBe("change input by trigger");
});
Copy the code

It can be seen that after the value is changed by input.element.value or setValue, the data in the VM is also changed due to the V-model binding relationship. We can also get the value of the input element via input.element.value.

For radio/checkbox-selected component elements, we can use the setChecked(Boolean) function to trigger a value change that also updates the v-model binding value on the element:

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"]);
});
Copy the code

For elements like buttons on which we want to trigger a click, we can do this using trigger:

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);
});
Copy the code

Custom events

For some components, it is possible to emit some return data via $emit. For example, we rewrite the Submit button in the Form above and click it to return some data:

{
  methods: {
    clickSubmit() {
      this.$emit("foo"."foo1"."foo2");
      this.$emit("bar"."bar1"); }},}Copy the code

In addition to triggering the click event of the element in the component for $emi, we can also trigger via wrapper.vm, since VM itself corresponds to the component’s this:

wrapper.vm.$emit("foo"."foo3");
Copy the code

Finally, all data returned by a $EMIT trigger is stored in wrapper.emitted(), which returns an object; The structure is as follows:

{
    foo: [['foo1'.'foo2' ], [ 'foo3']],bar: [['bar1']]}Copy the code

Emitted () returns an array of properties in an object, the length of which represents how many times the method is emitted; We can assert properties on the object to determine whether emit is emitted from the component:

test("test emit".async() = > {// The component element fires emit
  await wrapper.find('input[type="submit"]').trigger("click");
  wrapper.vm.$emit("foo"."foo3");
  await vm.$nextTick();
  // Foo is triggered
  expect(wrapper.emitted().foo).toBeTruthy();
  // Foo fires twice
  expect(wrapper.emitted().foo.length).toBe(2);
  // Assert the data foo first fires
  expect(wrapper.emitted().foo[0]).toEqual(["foo1"."foo2"]);
  // Baz does not trigger
  expect(wrapper.emitted().baz).toBeFalsy();
});
Copy the code

Even an adaptation of the emitted() function might be an option, rather than capturing the entire emitted object at once:

expect(wrapper.emitted('foo')).toBeTruthy();
expect(wrapper.emitted('foo').length).toBe(2);
Copy the code

There are some components that emit emit events that may be triggered by their children. We can emit from the vm of the child component:

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')})})Copy the code

Match the Vue – the Router

In some components, it is possible to use vue-router related components or Api methods. For example, we have a Header component:

<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>
Copy the code

$router-view {$router-view {$router-view {$router-view}} Vue.use(VueRouter) is not recommended because it pollutes the global Vue; There are two ways to do this. The first way is to use createLocalVue to create a Vue class where we can add components, mix, and install plug-ins without polluting the global Vue class:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import Header from "@/components/Header";

// a Vue class
const localVue = createLocalVue()
localVue.use(VueRouter)
// Route array
const routes = []
const router = new VueRouter({
  routes
})

shallowMount(Header, {
  localVue,
  router
})
Copy the code

Let’s take a look at what we’re doing here, creating a localVue with createLocalVue, which is equivalent to import Vue; Then localvue. use tells Vue to use VueRouter, which has the same effect as vue. use; Create a router object and pass shallowMount to it.

The second method is to inject forged data. Here, mocks and STUbs are mainly used. Mocks are used to forge route and Route and global objects such as route and router. Stubs override globally or locally registered components:

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");
  });
});
Copy the code

Compared with the first method, the second method is more operable, can directly forge $route data; Generally, the first method is not used alone, but is often paired with the second method of falsifying data.

Cooperate with Vuex

We usually use vuex in components. We can simulate testing by forging store data. Suppose we have a count component whose data is stored in 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>
Copy the code

In VUEX, we modify the number through mutations:

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; }}});Copy the code

So how do we fake store data now? The same principle applies to vue-router. Create an isolated Vue class using createLocalVue:

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();
  });
});
Copy the code

Let’s see what we’re doing here. We created an isolation class localVue just like VueRouter; Then create a Store using new vuex. Store and fill in the false data state and mutations; We don’t care what operations the mutations function does, we just need to know which mutations function is triggered by the click of the element, and we can use the forged function to determine whether mutations are called.

Another way to test store data is to create a running Store that no longer triggers functions in Vuex through the page. This has the advantage of not having to forge Vuex functions; Suppose we have a store/list.js

export default {
  state: {
    list: []},getters: {
    joinList: (state) = > {
      return state.list.join(","); }},mutations: {
    PUSH(state, payload){ state.list.push(payload); ,}}};Copy the code
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");
  });
});
Copy the code

We created a store directly to commit and getters.

conclusion

The front end framework is iterative, but the front end unit testing does show that someone cares; A robust front-end project should have unit-tested modules that ensure the stability of the project’s code quality and functionality; But not all projects need unit tests, because writing test cases costs money. So if your project meets the following criteria, consider introducing unit testing:

  • Long-term stable project iteration needs to ensure the maintainability and functional stability of the code;
  • Page function is relatively complex, logic more;
  • Consider unit testing for components that are highly reusable;

  

For more front-end information, please pay attention to the public number [front-end reading].

If you think it’s good, check out my Nuggets page. Please visit Xie xiaofei’s blog for more articles