In the article “The Path to Front-end Progression: Front-end Architecture Design (3) — The Testing Core”, the concept of TDD is introduced by analyzing “the limitations of traditional manual testing” and some testing tools. In this article, I will explain how to use Mocha & Karma through a Vue project, and how to do unit testing with Vue-test-utils, the official recommendation of Vue.

Installed a.

I wrote a sample library for this tutorial that you can skip the entire installation process and run the sample project after installing the dependencies:

If you want to go through the installation step by step, you can also follow these steps:

1. Initialize the Vue project using scaffolding (using Webpack templates)

// Enter vue init webpack vueunittest on the command line (the default reader of this article already has the vue-cli and node environment installed)

Note that when asked to Pick a test runner(Use arrow keys) at this step, select Karma and Mocha

The next step is to go into the project NPM install and install the dependencies (this step may even cause the phantomJS browser to fail to install, ignore it, because we will not be using this browser), then NPM run build.

Install karma-chrome-launch

Next, install karma-chrome-launcher and type it on the command line

npm install karma-chrome-launcher --save-dev

Don’t ask me why I don’t use PhantomJS, because there are often inscrutable errors, but I won’t use Chrome!!)

//karma.conf.js var webpackConfig = require('.. /.. /build/webpack.test.conf') module.exports = function (config) { config.set({ //browsers: ['PhantomJS'], browsers: ['Chrome'], ... })}

(c) Install Vue-Test-Utils

To install the official vue.js unit test utility library, type the following command line:

npm install --save-dev vue-test-utils

NPM run unit

When you have completed the above two steps, you can try your first unit test by executing NPM run unit from the command line. The Vue scaffold has initialized a test file with HelloWorld.spec.js to test HelloRold.vue, You can in the test/unit/specs/HelloWorld. Spec. Js find the test file. (note: all the test files in the future, will put the specs this directory, and to test the script name. Spec. Js end name!)

Type NPM run unit on the command line, and when you see the green screen shown in the image below, your unit test passes!

Two. The use of testing tools

Below is a Counter. Vue file that I will use as a basis to explain how to use the test tool in my project.

Vue <template> <div> <h3>Counter. Vue </h3> {{count}} <button @click="increment"> </button> </div> </template> <script> export default { data () { return { count: 0 } }, methods: { increment () { this.count++ } } } </script>

(I) Mocha framework

1. How to write the Mocha test script

Mocha is used to run a test script. To test Counter. Vue above, we need to write a test script. For example, the test script for the Counter.vue component should be named Counter.spec.js

//Counter.spec.js import Vue from 'vue' import Counter from '@/components/Counter' describe('Counter.vue', () => {it(' Click the button, count should be 1', () => {// const Constructor = vue.extend (Counter); // mount const vm = new Constructor().$mount(); $el.querySelector('button'); $el.querySelector('button'); // New ClickEvent const ClickEvent = new Window.Event('click'); // Trigger the clickEvent Button.DispatchEvent (ClickEvent); // Listen for click events vm._watcher.run(); Expect (Number(vm.$el.querySelector('.num').textContent)).to.equal(1); })})

The above code is a test script. The test script should contain one or more describe blocks, and each describe block should contain one or more IT blocks

The Describe block is called a Test Suite, which represents a set of related tests. It is a function with the first argument being the name of the test suite (usually written as the name of the test component, in this case Counter.js) and the second argument being the function that is actually executed.

An IT block, called a “test case,” represents a single test and is the smallest unit of tests. It is also a function, with the first argument being the name of the test case (which usually describes the result of your assertion, in this case, “When the button is clicked, count should be 1”) and the second argument being the function that is actually executed.

2. Mocha performs asynchronous tests

We add a button to the Counter. Vue component and add an asynchronous incrementing method to incrementByAsync, which sets a delay to count incrementing by 1 after 1000ms.

<template> ... <button @click="increment"> increment </button> <button @click="incrementByAsync"> increment </button>... <template> <script> ... methods: { ... incrementByAsync () { window.setTimeout(() => { this.count++; }, 1000) } } </script>

Add a new test case to the test script, namely it().

If ('count async, count = 1', (done) = bb0 {const Constructor = vue.extend (Counter); // mount const vm = new Constructor().$mount(); $el.querySelectorAll('button')[1]; // New ClickEvent const ClickEvent = new Window.Event('click'); // Trigger the clickEvent Button.DispatchEvent (ClickEvent); // Listen for click events vm._watcher.run(); Window.setTimeout (() => {window.setTimeout() => {window.setTimeout() => {window.setTimeout() => {window.setTimeout() => expect(Number(vm.$el.querySelector('.num').textContent)).to.equal(1); done(); }, 1000); })

In Mocha, you need to add a done parameter to the function in it(), and you must call done() after the asynchronous execution. If you do not call done(), Then Mocha will report an error after 2000ms and the unit test will fail (Mocha default asynchronous test timeout limit is 2000ms). The error message is as follows:

3. Mocha’s test hook

If you understand Vue’s mounted(), created() hooks, it’s also easy to understand Mocha’s hooks, which provide four hooks in the describe block: before(), after(), beforeEach(), afterEach(). They are executed at the following times

Describe (' hook specification ', function() {before(function() {// perform before all test cases in this block}); After (function() {// Execute after all test cases in this block}); BeforeEach (function() {// Execute beforeEach test case in this block}); AfterEach (function() {// Execute afterEach test case in this block}); });

These are the basics of how to use Mocha. If you want to learn more about how to use Mocha, you can check out the following documentation and one of Ruan’s tutorials on Mocha:

  • Mocha official documentation: https://mochajs.org/
  • Mocha official document translation: http://www.jianshu.com/p/9c78…
  • Nguyen other – test framework Mocha instance tutorial: http://www.ruanyifeng.com/blo…

(2) Chai assertion library

In the test case above, an assertion begins with the expect() method.

expect(Number(vm.$el.querySelector('.num').textContent)).to.equal(1);

Assertions determine whether the actual execution of the source code is consistent with the expected result, and if not, throw an error. The content of a node with a name such as.num should be the number 1. There are many kinds of assertion libraries, and Mocha doesn’t restrict which one you need to use. The assertion library provided by Vue’s scaffolding is Sino-Chai, which is a chai-based assertion library, and we specify its Expect assertion style.

The benefits of the Expect assertion style are close to those of natural languages, and here are some examples

Expect (1 + 1).to. Be. (2); expect(1 + 1).to.be.not.equal(3); // Boolean true expect('hello').to.be.ok; expect(false).to.not.be.ok; // typeof expect('test').to.be.a('string'); expect({ foo: 'bar' }).to.be.an('object'); expect(foo).to.be.an.instanceof(Foo); / / include expect ([1, 2, 3]). To. Include (2); expect('foobar').to.contain('foo'); expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo'); // empty expect([]).to.be.empty; expect('').to.be.empty; expect({}).to.be.empty; // match expect('foobar').to.match(/^foo/);

Every test case wrapped in it() should have one or more assertions. The above is just a partial introduction to the assertion syntax. If you want to know more about Chai’s assertion syntax, please see the official documentation below.

  • CHAI official document: http://chaijs.com/
  • Chai official document translation: http://www.jianshu.com/p/f200…

(3) Vue-Test-Utils test library

1. Introduce vue-test-utils into the test script

//Counter.spec.js

import Vue from 'vue'
import Counter from '@/components/Counter'
//引入vue-test-utils
import {mount} from 'vue-test-utils'

2. Test the text

Now I’m going to test the

text in Counter. Vue in the Counter. Spec.js test script. You can get a sense of how much easier it is to test the.vue single file component using vue-test-utils.

  • Test cases without vue-test-utils:
It (' not used vue-test-utils: correctly render h3 text to Counter. Vue ', () => {const Constructor = vue.extend (Counter); const vm = new Constructor().$mount(); const H3 = vm.$el.querySelector('h3').textContent; expect(H3).to.equal('Counter.vue'); })
  • Test case with vue-test-utils:
It (' use vue-test-utils: render h3 text Counter. Vue ', () => {const wrapper = mount(Counter); expect(wrapper.find('h3').text()).to.equal('Counter.vue'); })

As you can see from the above code, the vue-test-utils tool cuts the amount of code in half for this test case, especially if it is a more complex test case. It allows us to focus more on writing test logic to files, leaving the tedious task of getting component instances and mounting them to Vue-test-utils.

3. Common API for vue-test-utils

  • find(): Returns the first DOM node or Vue component that matches the selectorwrapper, you can use any valid selector
  • text()Returns thewrapperThe text content of
  • html()Returns thewrapper DOMHTML string of
It (' the find ()/text ()/HTML () method ', () = > {const wrapper = mount (Counter); const h3 = wrapper.find('h3'); expect(h3.text()).to.equal('Counter.vue'); expect(h3.html()).to.equal('<h3>Counter.vue</h3>'); })
  • trigger(): in thewrapper DOMAn event is emitted on the node.
It ('trigger() method ', () => {const wrapper = mount(Counter); const buttonOfSync = wrapper.find('.sync-button'); buttonOfSync.trigger('click'); buttonOfSync.trigger('click'); const count = Number(wrapper.find('.num').text()); expect(count).to.equal(2); })
  • setData()Set:dataAttribute and force update
It ('setData() method ',() => {const wrapper = mount(Counter); wrapper.setData({foo: 'bar'}); expect(wrapper.vm.foo).to.equal('bar'); })

Several of the methods that vue-test-utils provides are described above, but if you want to learn more about vue-test-utils, please read the official documentation below:

  • Vue-Test-Utils: https://vue-test-utils.vuejs https://vue-test-utils.vuejs

III. Project Description

The project mimics a simple tweet and, once downloaded from the code repository, can be run directly through NPM run dev.

(I) Project renderings

(II) Interactive logic and requirements in the project

  1. Enter the content in the text box and click the “publish” button (1). The newly published content will be added to the list of microblogs, and the number of microblogs (6) will be increased by one
  2. When there is no content in the text box, you cannot post an empty microblog to the microblog list, and a prompt box pops up asking the user to input content
  3. When you click “Follow “(2), the number of followers under your avatar (5) will be increased by 1, and the font in the button will change to” Unfollow “; When you click “Unfollow “(2), the number of personal profile pictures (5) will be reduced by 1, and the font inside the button will change to” Follow “.
  4. When clicking “Favorites “(3), My Favorites (7) will increase by 1, and the text inside the button will change to” Favorites “; When clicking “Favorites “(3), My Favorites (7) will be reduced by 1, and the text inside the button will change to” Favorites “.
  5. When I click “Like “(4), my” Like “(8) will increase by 1, and the text inside the button will change to “Unlike “; When I click “Unlike “(3), my” Like “(8) will be reduced by one number, and the text inside the button will become “Like”.

(3) Project source code

//SinaWeibo.vue <template> <div class="weibo-page"> <nav> <span class="weibo-logo"></span> <div class="search-wrapper"> <input type="text" placeholder=" placeholder "; > <img v-if="! iconActive" @mouseover="mouseOverToIcon" src=".. /.. /static/image/search.png" Alt =" Icon" > <img v-if=" IconActive "@mouseoutToIcon" SRC ="... /.. /static/image/search-active. PNG "Alt =" icon"> </div> </nav> <div class="main-container"> <aside class="aside "> <ul>  <li :class="{ active: isActives[indexOfContent] }" v-for="(content, indexOfContent) in asideTab" :key="indexOfContent" @click="tabChange(indexOfContent)"> <span>{{content}}</span> <span class="count"> <span v-if="indexOfContent === 1">({{collectNum}})</span> <span v-if="indexOfContent === 2">({{likeNum}})</span> </span> </li> </ul> </aside> <main class="weibo-content"> <div class="weibo-publish-wrapper"> <img src=".. /.. /static/image/tell-people.png"></img> <textarea v-model="newWeiboContent.content"></textarea> <button @click=" PublishNewWeiboContent "@click=" PublishNewWeiboContent" </button> </div> <div class="weibo-news" v-for="(news, indexOfNews) in weiboNews" :key="indexOfNews"> <div class="content-wrapper"> <div class="news-title"> <div class="news-title__left"> <img :src="news.imgUrl"> <div class="title-text"> <div class="title-name">{{news.name}}</div> <div class="title-time">{{news.resource}}</div> </div> </div> <button class="news-title__right add" v-if="news.attention === false" @click=" Attention (indexOfNews)"> < I class="fa fa-plus"></ I ></ button class="news-title__right "V-if ="news.attention === true" @Click ="unAttention(indexOfNews)"> < I class="fa fa-close"></ I ></ button>" v-if="news.attention === true" @Click ="unAttention(indexOfNews)"> < I class="fa fa-close"></ I ></ button> </div> <div class="news-content">{{news.content}}</div> <div class="news-image" v-if="news.images.length"> <img v-for="(img, indexOfImg) in news.images" :key="indexOfImg" :src="img"> </div> </div> <ul class="news-panel"> <li @click="handleCollect(indexOfNews)"> <i class="fa fa-star-o" :class="{collected: News.collect}"></ I > {{news.collect? ": 'collection'}} < / li > < li > < I class = "fa fa - external - link" > < / I > forward < / li > < li > < I class = "fa fa - the commenting - o" > < / I > comments < / li > < li @click="handleLike(indexOfNews)"> < I class="fa fa-thumbs-o-up" :class="{news.like}"></ I > {{news.like? 'Unliked' : } </div> <div class="profile-top"> </div> <div class="profile-top"> </div> <div class="profile-top"> </div <img src=".. /.. /static/image/profile.jpg"> </div> <div class="profile-bottom"> <div class="profile-name">Lee_tanghui</div> <ul class="profile-info"> <li v-for="(profile, indexOfProfile) in profileData" :key="indexOfProfile"> <div class="number">{{profile.num}}</div> <div class="text">{{profile.text}}</div> </li> </ul> </div> </div> </aside> </div> <footer> Wish you like my blog! LITANGHUI </footer> </div> </template> <script> // MockData import * as mockData from '.. /mock-data.js' export default {mounted() {this.profileData = mockData.profiledata; this.weiboNews = mockData.weiboNews; this.collectNum = mockData.collectNum; this.likeNum = mockData.likeNum; }, data() {return {iconActive: false, asIDetab: [" Home ", "My Favorites "," My Lives "], isActives: [true, false, false], profileData: [], weiboNews: [], collectNum: 0, likeNum: 0, newWeiboContent: { imgUrl: '.. /.. / static/image/profile. JPG ', name: 'Lee_tanghui', the resource: 'just from the web version of weibo', content: ' 'the images: []},}}, the methods: { mouseOverToIcon() { this.iconActive = true; }, mouseOutToIcon() { this.iconActive = false; }, tabChange(indexOfContent) { this.isActives.forEach((item, index) => { index === indexOfContent ? this.$set(this.isActives, index, true) : this.$set(this.isActives, index, false); }) }, publishNewWeiboContent() { if(! This. NewWeiboContent. Content) {alert (" please input the content! ') return; } const newWeibo = JSON.parse(JSON.stringify(this.newWeiboContent)); this.weiboNews.unshift(newWeibo); this.newWeiboContent.content = ''; this.profileData[2].num++; }, attention(index) { this.weiboNews[index].attention = true; this.profileData[0].num++; }, unAttention(index) { this.weiboNews[index].attention = false; this.profileData[0].num--; }, handleCollect(index) { this.weiboNews[index].collect = ! this.weiboNews[index].collect; this.weiboNews[index].collect ? this.collectNum++ : this.collectNum--; }, handleLike(index) { this.weiboNews[index].like = ! this.weiboNews[index].like; this.weiboNews[index].like ? this.likeNum++ : this.likeNum--; }}} < / script > < style lang = "less" > / / CSS part slightly < / style >

IV. Project unit test script practice

We will write test scripts for sinaweibo. vue based on the “interactive logic and requirements in the project” mentioned above. Here I will show the process of writing test cases:

1. Enter the content in the text box and click the “publish” button (1). The newly published content will be added to the list of microblogs, and the number of microblogs (6) will be increased by one

If () => {const wrapper = mount(SinaWeibo); const textArea = wrapper.find('.weibo-publish-wrapper textarea'); const buttonOfPublish = wrapper.find('.weibo-publish-wrapper button'); const lengthOfWeiboNews = wrapper.vm.weiboNews.length; const countOfMyWeibo = wrapper.vm.profileData[2].num; // Set TextArea binding data Wrapper. setData({newWeiBoContent: {imgURL: '.. '). /.. / static/image/profile. JPG ', name: 'Lee_tanghui', the resource: 'just from the web version of weibo', content: 'welcome to my weibo', images: []}}); // ButtonOfPublish.trigger ('click'); const lengthOfWeiboNewsAfterPublish = wrapper.vm.weiboNews.length; const countOfMyWeiboAfterPublish = wrapper.vm.profileData[2].num; / / assertion: to issue the new content expect (lengthOfWeiboNewsAfterPublish). To. Equal (lengthOfWeiboNews + 1); / / assertion: personal weibo number 1 expect (countOfMyWeiboAfterPublish). To. Equal (countOfMyWeibo + 1); })

Test results:

2. When there is no content in the text box, you cannot post an empty microblog into the microblog list, and a prompt box pops up to ask the user to input content

It (', () => {const wrapper = mount(SinaWeibo); const textArea = wrapper.find('.weibo-publish-wrapper textarea'); const buttonOfPublish = wrapper.find('.weibo-publish-wrapper button'); const lengthOfWeiboNews = wrapper.vm.weiboNews.length; const countOfMyWeibo = wrapper.vm.profileData[2].num; // Set TextArea binding data to empty Wrapper. setData({newWeiBoContent: {imgUrl: '.. '). /.. / static/image/profile. JPG ', name: 'Lee_tanghui', the resource: 'just from the web version of weibo', content: ' 'the images: []}}); // ButtonOfPublish.trigger ('click'); const lengthOfWeiboNewsAfterPublish = wrapper.vm.weiboNews.length; const countOfMyWeiboAfterPublish = wrapper.vm.profileData[2].num; / / asserts that did not release new content expect (lengthOfWeiboNewsAfterPublish). To. Equal (lengthOfWeiboNews); / / assertion: personal weibo number unchanged expect (countOfMyWeiboAfterPublish). To. Equal (countOfMyWeibo); })

Test results:

3. When clicking “Follow “(2), the number of followers under your avatar (5) will be increased by 1, and the font in the button will change to” Unfollow “; When you click “Unfollow “(2), the number of personal profile pictures (5) will be reduced by 1, and the font inside the button will change to” Follow “.

It (" When you click "follow ", the number of followers increases by 1 and the font inside the button changes to" unfollow "", () => {const wrapper = mount(SinaWeibo); const buttonOfAddAttendion = wrapper.find('.add'); const countOfMyAttention = wrapper.vm.profileData[0].num; / / triggers buttonOfAddAttendion. The trigger (" click "); const countOfMyAttentionAfterClick = wrapper.vm.profileData[0].num; / / asserts that the amount of attention under the person's image will increase 1 expect (countOfMyAttentionAfterClick). To. Equal (countOfMyAttention + 1); / / asserts that button font in the "cancel attention to expect (buttonOfAddAttendion. The text ()). To. Equal (' cancel attention '); })
It (" When you click "Unfollow ", the number of followers is reduced by 1 and the font inside the button becomes" Follow ", () => {const wrapper = mount(SinaWeibo); const buttonOfUnAttendion = wrapper.find('.cancel'); const countOfMyAttention = wrapper.vm.profileData[0].num; / / triggers buttonOfUnAttendion. The trigger (" click "); const countOfMyAttentionAfterClick = wrapper.vm.profileData[0].num; / / asserts that the amount of attention under the person's image will increase 1 expect (countOfMyAttentionAfterClick). To. Equal (countOfMyAttention - 1); // assert: Button inside font changes to "Unfocus expect(buttonofunattendion.text ()).to. Equal (' focus '); })

Test results:

4. When clicking “Favorites “(3), my favorites (7) will increase by 1, and the text inside the button will change to” Favorites “; When clicking “Favorites “(3), My Favorites (7) will be reduced by 1, and the text inside the button will change to” Favorites “.

It (" My favorite increases by 1 and the text inside the button becomes "Bookmarks" ", () => {const wrapper = mount(SinaWeibo); const buttonOfCollect = wrapper.find('.collectWeibo'); const countOfMyCollect = Number(wrapper.find('.collect-num span').text()); // ButtonOfCollect.trigger ('click'); const countOfMyCollectAfterClick = Number(wrapper.find('.collect-num span').text()); / / assertion: my collection number will add 1 expect (countOfMyCollectAfterClick). To. Equal (countOfMyCollect + 1); Expect (ButtonofCollect. Text ()).to.equal(' Collect '); })
It (" My favorite drops by 1 when I click "Bookmark", () => {const wrapper = mount(SinaWeibo); const buttonOfUnCollect = wrapper.find('.uncollectWeibo'); const countOfMyCollect = Number(wrapper.find('.collect-num span').text()); / / triggers the click event buttonOfUnCollect. The trigger (" click "); const countOfMyCollectAfterClick = Number(wrapper.find('.collect-num span').text()); / / assertion: my collection quantity will be reduced 1 expect (countOfMyCollectAfterClick). To. Equal (countOfMyCollect - 1); Expect (ButtonOfUnCollect. Text ()).to. Equal (' Collect '); })

Test results:

5. When I click “Like “(4), my” Like “(8) will increase by 1 number, and the text in the button will change to “Unlike “; When I click “Unlike “(3), my” Like “(8) will be reduced by one number, and the text inside the button will become “Like”.

If () => {const wrapper = mount(SinaWeibo); if () = 0 {const wrapper = mount(SinaWeibo); const buttonOfLike = wrapper.find('.dislikedWeibo'); const countOfMyLike = Number(wrapper.find('.like-num span').text()); // trigger the click event buttonOfLike.trigger('click'); const countOfMyLikeAfterClick = Number(wrapper.find('.like-num span').text()); Expect (countOfMyLikeAfterClick).to. Equal (countOfMyLike + 1); // assert: Button text changes to Unlike Expect (buttonOfLike.text()).to.equal(' Unlike '); });
It (" When I click "Unlike ", my" Like "becomes" Like "", () => {const wrapper = mount(SinaWeibo); const buttonOfDislike = wrapper.find('.likedWeibo'); const countOfMyLike = Number(wrapper.find('.like-num span').text()); // trigger the click event buttonOfDiske.trigger ('click'); const countOfMyLikeAfterClick = Number(wrapper.find('.like-num span').text()); Expect (countOfMyLikeAfterClick).to. Equal (countofmylike-1); Expect (buttonOfDislike. Text ()).to. Equal (' like '); });

The test results

The project address:

Git repository:
https://github.com/Lee-Tanghu…

Refer to the article

  1. Testing Framework Mocha Instance Tutorial – Ruan Yifeng
  2. Chai.js Assertion Library API Chinese documentation
  3. Zhihu: If you unit test Vue
  4. Vue.js Learning Series 6 — Vue Unit Tests Karma+Mocha Learning Notes