前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)

在使用vue-cli创建项目的时候,会提示要不要安装单元测试和e2e测试。这篇文章我将通过一个Vue的项目, 去讲解如何使用mocha & karma, 且结合vue官方推荐的vue-test-utils去进行单元测试的实战.

简介

Karma

Karma是一个基于Node.js的JavaScript测试执行过程管理工具(Test Runner)。该工具在Vue中的主要作用是将项目运行在各种主流Web浏览器进行测试。
换句话说,它是一个测试工具,能让你的代码在浏览器环境下测试。需要它的原因在于,你的代码可能是设计在浏览器端执行的,在node环境下测试可能有些bug暴露不出来;另外,浏览器有兼容问题,karma提供了手段让你的代码自动在多个浏览器(chrome,firefox,ie等)环境下运行。如果你的代码只会运行在node端,那么你不需要用karma。

Mocha

Mocha是一个测试框架,在vue-cli中配合chai断言库实现单元测试。
Mocha的常用命令和用法不算太多,看阮一峰老师的测试框架 Mocha 实例教程就可以大致了解了。
而Chai断言库可以看Chai.js断言库API中文文档,很简单,多查多用就能很快掌握。

npm run unit 执行过程

  1. 执行 npm run unit 命令
  2. 开启Karma运行环境
  3. 使用Mocha去逐个测试用Chai断言写的测试用例
  4. 在终端显示测试结果
  5. 如果测试成功,karma-coverage 会在 ./test/unit/coverage 文件夹中生成测试覆盖率结果的网页。

一. 安装

我为本教程写一个示例库, 您可以直接跳过所有安装过程, 安装依赖后运行该示例项目:
如果想一步步进行安装, 也可以跟着下面的步骤进行操作:

(一) 使用脚手架初始化vue项目(使用webpack模板)

//命令行中输入(默认阅读该文章的读者已经安装vue-cli和node环境)
vue init webpack vueunittest

注意, 当询问到这一步Pick a test runner(Use arrow keys)时, 请选择使用Karma and Mocha
前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)_第1张图片
下面是用 npm install -g vue-cli 构建项目时可以直接引入测试模块,如下:
前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)_第2张图片
项目构建之后会有这么一个test模块,结构如下:
前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)_第3张图片
接下来的操作进入项目npm install安装相关依赖后(该步骤可能更会出现PhantomJS这个浏览器安装失败的报错, 不用理会, 因为 之后我们不使用这个浏览器), npm run build即可.

(二) 安装Karma-chrome-launch

接下来安装karma-chrome-launcher, 在命令行中输入

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

对于Karma,我只是了解了一下它的配置选项。
下面是Vue的karma配置,简单注释了下:
然后在项目中找到test/unit/karma.conf.js文件, 将PhantomJS浏览器修改为Chrome不要问我为什么不使用PhantomJS, 因为经常莫名的错误, 改成Chrome就不会!!!)

//karma.conf.js

'use strict'

const path = require('path')
const merge = require('webpack-merge')
const webpack = require('webpack')

const baseConfig = require('../../.electron-vue/webpack.renderer.config')
const projectRoot = path.resolve(__dirname, '../../src/renderer')

// Set BABEL_ENV to use proper preset config
process.env.BABEL_ENV = 'test'

let webpackConfig = merge(baseConfig, {
    devtool: '#inline-source-map',
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': '"testing"'
        })
    ]
})

// don't treat dependencies as externals
delete webpackConfig.entry
delete webpackConfig.externals
delete webpackConfig.output.libraryTarget

// apply vue option to apply isparta-loader on js
webpackConfig.module.rules
    .find(rule => rule.use.loader === 'vue-loader').use.options.loaders.js = 'babel-loader'

module.exports = config => {
    config.set({
        // 浏览器
    	browsers: ['PhantomJS'],
 	    browsers: ['Chrome'],
    	// electron
        browsers: ['visibleElectron'],
        client: {
            useIframe: false
        },
        // 测试覆盖率报告
        // https://github.com/karma-runner/karma-coverage/blob/master/docs/configuration.md
        coverageReporter: {
            dir: './coverage',
            reporters: [
                { type: 'lcov', subdir: '.' },
                { type: 'text-summary' }
            ]
        },
        customLaunchers: {
            'visibleElectron': {
                base: 'Electron',
                flags: ['--show']
            }
        },
        // 测试框架
        frameworks: ['mocha', 'chai'],
        // 测试入口文件
        files: ['./index.js'],
        // 预处理器 karma-webpack
        preprocessors: {
            './index.js': ['webpack', 'sourcemap']
        },
        // 测试报告
        reporters: ['spec', 'coverage'],
        singleRun: true,
        // Webpack配置
        webpack: webpackConfig,
        // Webpack中间件
        webpackMiddleware: {
            noInfo: true
        }
    })
}

Mocha和chai

我们看下官方的例子(都用注释来解释代码意思了):

import Vue from 'vue' // 导入Vue用于生成Vue实例
import Hello from '@/components/Hello' // 导入组件
// 测试脚本里面应该包括一个或多个describe块,称为测试套件(test suite)
describe('Hello.vue', () => {
  // 每个describe块应该包括一个或多个it块,称为测试用例(test case)
  it('should render correct contents', () => {
    const Constructor = Vue.extend(Hello) // 获得Hello组件实例
    const vm = new Constructor().$mount() // 将组件挂在到DOM上
    //断言:DOM中class为hello的元素中的h1元素的文本内容为Welcome to Your Vue.js App
    expect(vm.$el.querySelector('.hello h1').textContent)
      .to.equal('Welcome to Your Vue.js App')  
  })
})

(三) 安装Vue-test-utils

安装Vue.js 官方的单元测试实用工具库, 在命令行输入:
npm install --save-dev vue-test-utils

(四) 执行npm run unit

当你完成以上两步的时候, 你就可以在命令行执行npm run unit尝鲜你的第一次单元测试了, Vue脚手架已经初始化了一个HelloWorld.spec.js的测试文件去测试HelloWrold.vue, 你可以在test/unit/specs/HelloWorld.spec.js下找到这个测试文件.(提示: 将来所有的测试文件, 都将放specs这个目录下, 并以测试脚本名.spec.js结尾命名!)
在命令行输入npm run unit, 当你看到下图所示的一篇绿的时候, 说明你的单元测试通过了!
前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)_第4张图片
第一次单元测试测试通过

需要知道的知识点:
  • 测试脚本都要放在test/unit/specs/ 目录下。
  • 脚本命名方式为[组件名].spec.js
  • 所谓断言,就是对组件做一些操作,并预言产生的结果。如果测试结果与断言相同则测试通过。
  • 单元测试默认测试 src 目录下除了 main.js之外的所有文件,可在test/unit/index.js文件中修改。
  • Chai断言库中,to be been is that which and has have with at of same这些语言链是没有意义的,只是便于理解而已。
  • 测试脚本由多个 descibe 组成,每个 describe 由多个it 组成。

二. 测试工具的使用方法

下面是一个Counter.vue文件, 我将以该文件为基础讲解项目中测试工具的使用方法.

//Counter.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>

(一) Mocha框架

1. Mocha测试脚本的写法

Mocha的作用是运行测试脚本, 要对上面Counter.vue进行测试, 我们就要写测试脚本, 通常测试脚本应该与Vue组件名相同, 后缀为spec.js. 比如, Counter.vue组件的测试脚本名字就应该为Counter.spec.js

//Counter.spec.js

import Vue from 'vue'
import Counter from '@/components/Counter'

describe('Counter.vue', () => {

  it('点击按钮后, count的值应该为1', () => {
    //获取组件实例
    const Constructor = Vue.extend(Counter);
    //挂载组件
    const vm = new Constructor().$mount();
    //获取button
    const button = vm.$el.querySelector('button');
    //新建点击事件
    const clickEvent = new window.Event('click');
    //触发点击事件
    button.dispatchEvent(clickEvent);
    //监听点击事件
    vm._watcher.run();
    // 断言:count的值应该是数字1
    expect(Number(vm.$el.querySelector('.num').textContent)).to.equal(1);
  })
})

上面这段代码就是一个测试脚本.测试脚本应该包含一个或多个describe, 每个describe块应该包括一个或多个it
describe块称为"测试套件"(test suite), 表示一组相关的测试. 它是一个函数, 第一个参数是测试套件的名称(通常写测试组件的名称, 这里即为Counter.js), 第二个参数是一个实际执行的函数.
it块称为"测试用例"(test case), 表示一个单独的测试, 是测试的最小单位. 它也是一个函数, 第一个参数是测试用例的名称(通常描述你的断言结果, 这里即为"点击按钮后, count的值应该为1"), 第二个参数是一个实际执行的函数.

2. Mocha进行异步测试

我们在Counter.vue组件中添加一个按钮, 并添加一个异步自增的方法为incrementByAsync, 该函数设置一个延时器, 1000ms后count自增1.

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

给测试脚本中新增一个测试用例, 也就是it()

  it('count异步更新, count的值应该为1', (done) => {
    ///获取组件实例
    const Constructor = Vue.extend(Counter);
    //挂载组件
    const vm = new Constructor().$mount();
    //获取button
    const button = vm.$el.querySelectorAll('button')[1];
    //新建点击事件
    const clickEvent = new window.Event('click');

    //触发点击事件
    button.dispatchEvent(clickEvent);
    //监听点击事件
    vm._watcher.run();
    //1s后进行断言
    window.setTimeout(() => {
      // 断言:count的值应该是数字1
      expect(Number(vm.$el.querySelector('.num').textContent)).to.equal(1);
      done();
    }, 1000);
  })

Mocha中的异步测试, 需要给it()内函数的参数中添加一个done, 并在异步执行完后必须调用done(), 如果不调用done(), 那么Mocha会在2000ms后报错且本次单元测试测试失败(mocha默认的异步测试超时上线为2000ms), 错误信息如下:
未调用done()的报错
未调用done()的报错

3. Mocha的测试钩子

如果大家对于vue的mounted(), created()钩子能够理解的话, 对Mocha的钩子也很容易理解, Mocha在describe块中提供了四个钩子: before(), after(), beforeEach(), afterEach(). 它们会在以下时间执行

describe('钩子说明', function() {

  before(function() {
    // 在本区块的所有测试用例之前执行
  });

  after(function() {
    // 在本区块的所有测试用例之后执行
  });

  beforeEach(function() {
    // 在本区块的每个测试用例之前执行
  });

  afterEach(function() {
    // 在本区块的每个测试用例之后执行
  });

});

上述就是Mocha的基本使用介绍, 如果想了解Mocha的更多使用方法, 可以查看下面的文档和一篇阮一峰的Mocha教程:

Mocha官方文档 : https://mochajs.org/

Mocha官方文档翻译 : http://www.jianshu.com/p/9c78548caffa

阮一峰 - 测试框架 Mocha 实例教程 : http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html

(二) Chai断言库

上面的测试用例中, 以expect()方法开头的就是断言.

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

所谓断言, 就是判断源码的实际执行结果与预期结果是否一致, 如果不一致, 就会抛出错误. 上面的断言的意思是指: 有.num这类名的节点的内容应该为数字1. 断言库库有很多种, Mocha并不限制你需要使用哪一种断言库, Vue的脚手架提供的断言库是sino-chai, 是一个基于Chai的断言库, 并且我们指定使用的是它的expect断言风格.
expect断言风格的优点很接近于自然语言, 下面是一些例子

  // 相等或不相等
  expect(1 + 1).to.be.equal(2);
  expect(1 + 1).to.be.not.equal(3);

  // 布尔值为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/);

每一个it()所包裹的测试用例都应该有一句或多句断言,上面只是介绍了一部分的断言语法, 如果想要知道更多Chai的断言语法, 请查看以下的官方文档.

Chai官方文档: http://chaijs.com/

Chai官方文档翻译: http://www.jianshu.com/p/f200a75a15d2

(三) Vue-test-utils测试库

1. 在测试脚本中引入vue-test-utils
//Counter.spec.js

import Vue from 'vue'
import Counter from '@/components/Counter'
//引入vue-test-utils
import {mount} from 'vue-test-utils'
2. 测试文本内容

下面我将在Counter.spec.js测试脚本中对Counter.vue中

的文本内容进行测试, 大家可以直观的感受一下使用了Vue-test-utils后对.vue单文件组件的测试变得多么简单.

未使用vue-test-utils的测试用例:

  it('未使用Vue-test-utils: 正确渲染h3的文字为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');
  })

使用了vue-test-utils的测试用例:

  it('使用Vue-test-Utils: 正确渲染h3的文字为Counter.vue', () => {
    const wrapper = mount(Counter);
    expect(wrapper.find('h3').text()).to.equal('Counter.vue');
  })

从上面的代码可以看出, vue-test-utils工具将该测试用例的代码量减少了一半, 如果是更复杂的测试用例, 那么代码量的减少将更为突出. 它可以让我们更专注于去写文件的测试逻辑, 将获取组件实例和挂载的繁琐的操作交由vue-test-utils去完成.

3. vue-test-utils的常用API
  • find(): 返回匹配选择器的第一个DOM节点或Vue组件的wrapper, 可以使用任何有效的选择器
  • text(): 返回wrapper的文本内容
  • html(): 返回wrapper DOM的HTML字符串
  it('find()/text()/html()方法', () => {
    const wrapper = mount(Counter);
    const h3 = wrapper.find('h3');
    expect(h3.text()).to.equal('Counter.vue');
    expect(h3.html()).to.equal('

Counter.vue

'
); })
  • trigger(): 在该 wrapper DOM 节点上触发一个事件。
it('trigger()方法', () => {
    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(): 设置data的属性并强制更新
  it('setData()方法',() => {
    const wrapper = mount(Counter);
    wrapper.setData({foo: 'bar'});
    expect(wrapper.vm.foo).to.equal('bar');
  })

上面介绍了几个vue-test-utils提供的方法, 如果想深入学习vue-test-utils, 请阅读下面的官方文档:

vue-test-utils官方文档: https://vue-test-utils.vuejs.org/zh-cn/

三. 项目说明

该项目模仿了一个简单的微博, 在代码仓库下载后, 可直接通过npm run dev运行.

(一) 项目效果图

前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)_第5张图片
项目展示

(二) 项目中的交互逻辑和需求

在文本框中输入内容后点击"发布"按钮(1), 会新发布内容到微博列表中, 且个人头像等下的微博数量(6)会增加1个
当文本框中无内容时, 不能发布空微博到微博列表, 且弹出提示框, 叫用户输入内容
当点击"关注"(2), 个人头像下关注的数量(5)会增加1个, 且按钮内字体变成"取消关注"; 当点击"取消关注"(2), 个人头像下的数量(5)会减少1个, 且按钮内字体变成"关注"
当点击"收藏"(3)时, 我的收藏(7)会增加1个数量, 且按钮内文字变成"已收藏"; 点击"已收藏"(3)时, 我的收藏(7)会减少1个数量, 且按钮内文字变成"收藏"
当点击"赞"(4), 我的赞(8)会增加1个数量, 且按钮内文字变成"取消赞"; 点击"取消赞"(3)时, 我的赞(8)会减少1个数量, 且按钮内文字变成"赞"

(三) 项目源码

//SinaWeibo.vue

<template>
  <div class="weibo-page">
    <nav>
      <span class="weibo-logo"></span>
      <div class="search-wrapper">
        <input type="text" placeholder="大家正在搜: 李棠辉的文章好赞!">
        <img v-if="!iconActive" @mouseover="mouseOverToIcon" src="../../static/image/search.png" alt="搜索icon">
        <img v-if="iconActive" @mouseout="mouseOutToIcon" src="../../static/image/search-active.png" alt="搜索icon">
      </div>
    </nav>
    <div class="main-container">
      <aside class="aside-nav">
        <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">发布</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>
             <button 
              class="news-title__right cancel" 
              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 ? "已收藏" : '收藏'}}
            </li>
            <li>
              <i class="fa fa-external-link"></i>
              转发
            </li>
            <li>
              <i class="fa fa-commenting-o"></i>
              评论
            </li>
            <li @click="handleLike(indexOfNews)">
              <i class="fa fa-thumbs-o-up" :class="{liked: news.like}"></i>
              {{news.like ? '取消赞' : '赞'}}
            </li>
          </ul>
        </div>

      </main>
      <aside class="aside-right">
        <div class="profile-wrapper">
          <div class="profile-top">
            <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>
  //引入假数据
  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: ["首页", "我的收藏", "我的赞"],
        isActives: [true, false, false],
        profileData: [],
        weiboNews: [],
        collectNum: 0,
        likeNum: 0,
        newWeiboContent:   {
          imgUrl: '../../static/image/profile.jpg',
          name: 'Lee_tanghui',
          resource: '刚刚 来自 网页版微博',
          content: '',
          images: []
        },
      }
    },
    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('请输入内容!')
          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部分略
</style>

四. 项目单元测试脚本实战

我们将以上文提到的"项目中的交互逻辑和需求"为基础, 为SinaWeibo.vue编写测试脚本, 下面我将展示测试用例编写过程:

1.在文本框中输入内容后点击"发布"按钮(1), 会新发布内容到微博列表中, 且个人头像等下的微博数量(6)会增加1个
it('点击发布按钮,发布新内容&个人微博数量增加1个', () => {
    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;
    
    //设置textArea的绑定数据
    wrapper.setData({newWeiboContent: {
      imgUrl: '../../static/image/profile.jpg',
      name: 'Lee_tanghui',
      resource: '刚刚 来自 网页版微博',
      content: '欢迎来到我的微博', 
      images: []
    }});
    //触发点击事件
    buttonOfPublish.trigger('click');
    const lengthOfWeiboNewsAfterPublish = wrapper.vm.weiboNews.length;
    const countOfMyWeiboAfterPublish = wrapper.vm.profileData[2].num;

    //断言: 发布新内容
    expect(lengthOfWeiboNewsAfterPublish).to.equal(lengthOfWeiboNews + 1);
    //断言: 个人微博数量增加1个
    expect(countOfMyWeiboAfterPublish).to.equal(countOfMyWeibo + 1);
    
  })

测试结果:
在这里插入图片描述
通过测试

2.当文本框中无内容时, 不能发布空微博到微博列表, 且弹出提示框, 叫用户输入内容
 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;
    
    //设置textArea的绑定数据为空
    wrapper.setData({newWeiboContent: {
      imgUrl: '../../static/image/profile.jpg',
      name: 'Lee_tanghui',
      resource: '刚刚 来自 网页版微博',
      content: '', 
      images: []
    }});
    //触发点击事件
    buttonOfPublish.trigger('click');
    const lengthOfWeiboNewsAfterPublish = wrapper.vm.weiboNews.length;
    const countOfMyWeiboAfterPublish = wrapper.vm.profileData[2].num;

    //断言: 没有发布新内容
    expect(lengthOfWeiboNewsAfterPublish).to.equal(lengthOfWeiboNews);
    //断言: 个人微博数量不变
    expect(countOfMyWeiboAfterPublish).to.equal(countOfMyWeibo);
  })

测试结果:
前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)_第6张图片

3.当点击"关注"(2), 个人头像下关注的数量(5)会增加1个, 且按钮内字体变成"取消关注"; 当点击"取消关注"(2), 个人头像下的数量(5)会减少1个, 且按钮内字体变成"关注"
  it('当点击"关注", 个人头像下关注的数量会增加1个, 且按钮内字体变成"取消关注"', () => {
    const wrapper = mount(SinaWeibo);
    const buttonOfAddAttendion = wrapper.find('.add');
    const countOfMyAttention = wrapper.vm.profileData[0].num;
  
    //触发事件
    buttonOfAddAttendion.trigger('click');
    
    const countOfMyAttentionAfterClick = wrapper.vm.profileData[0].num;

    //断言: 个人头像下关注的数量会增加1个
    expect(countOfMyAttentionAfterClick).to.equal(countOfMyAttention + 1);
    //断言: 按钮内字体变成"取消关注
    expect(buttonOfAddAttendion.text()).to.equal('取消关注');
  })
  it('当点击"取消关注", 个人头像下关注的数量会减少1个, 且按钮内字体变成"关注"', () => {
    const wrapper = mount(SinaWeibo);
    const buttonOfUnAttendion = wrapper.find('.cancel');
    const countOfMyAttention = wrapper.vm.profileData[0].num;
  
    //触发事件
    buttonOfUnAttendion.trigger('click');
    
    const countOfMyAttentionAfterClick = wrapper.vm.profileData[0].num;

    //断言: 个人头像下关注的数量会增加1个
    expect(countOfMyAttentionAfterClick).to.equal(countOfMyAttention - 1);
    //断言: 按钮内字体变成"取消关注
    expect(buttonOfUnAttendion.text()).to.equal('关注');
  })

测试结果:
前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)_第7张图片

4.当点击"收藏"(3)时, 我的收藏(7)会增加1个数量, 且按钮内文字变成"已收藏"; 点击"已收藏"(3)时, 我的收藏(7)会减少1个数量, 且按钮内文字变成"收藏"
  it('当点击"收藏"时, 我的收藏会增加1个数量, 且按钮内文字变成"已收藏"', () => {
    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());

    //断言: 我的收藏数量会加1
    expect(countOfMyCollectAfterClick).to.equal(countOfMyCollect + 1);
    //断言: 按钮内文字变成已收藏
    expect(buttonOfCollect.text()).to.equal('已收藏');
  })
  it('当点击"已收藏"时, 我的收藏会减少1个数量, 且按钮内文字变成"收藏"', () => {
    const wrapper = mount(SinaWeibo);
    const buttonOfUnCollect = wrapper.find('.uncollectWeibo');
    const countOfMyCollect = Number(wrapper.find('.collect-num span').text());

    //触发点击事件
    buttonOfUnCollect.trigger('click');
    const countOfMyCollectAfterClick = Number(wrapper.find('.collect-num span').text());

    //断言: 我的收藏数量会减1
    expect(countOfMyCollectAfterClick).to.equal(countOfMyCollect - 1 );
    //断言: 按钮内文字变成已收藏
    expect(buttonOfUnCollect.text()).to.equal('收藏');
  })

测试结果:
前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)_第8张图片

5.当点击"赞"(4), 我的赞(8)会增加1个数量, 且按钮内文字变成"取消赞"; 点击"取消赞"(3)时, 我的赞(8)会减少1个数量, 且按钮内文字变成"赞"
  it('当点击"赞", 我的赞会增加1个数量, 且按钮内文字变成"取消赞"', () => {
    const wrapper = mount(SinaWeibo);
    const buttonOfLike = wrapper.find('.dislikedWeibo');
    const countOfMyLike = Number(wrapper.find('.like-num span').text());

    //触发点击事件
    buttonOfLike.trigger('click');
    const countOfMyLikeAfterClick = Number(wrapper.find('.like-num span').text());

    //断言: 我的赞会增加1个数量
    expect(countOfMyLikeAfterClick).to.equal(countOfMyLike + 1);
    //断言: 按钮内文字变成取消赞
    expect(buttonOfLike.text()).to.equal('取消赞');
  });
it('当点击"取消赞", 我的赞会减少1个数量, 且按钮内文字变成"赞"', () => {
    const wrapper = mount(SinaWeibo);
    const buttonOfDislike = wrapper.find('.likedWeibo');
    const countOfMyLike = Number(wrapper.find('.like-num span').text());

    //触发点击事件
    buttonOfDislike.trigger('click');
    const countOfMyLikeAfterClick = Number(wrapper.find('.like-num span').text());

    //断言: 我的赞会增加1个数量
    expect(countOfMyLikeAfterClick).to.equal(countOfMyLike - 1);
    //断言: 按钮内文字变成取消赞
    expect(buttonOfDislike.text()).to.equal('赞');
  });

测试结果
前端单元测试框架(Karma/Mocha + Vue-Test-Utils + Chai)_第9张图片

项目地址:

Git仓库: https://github.com/Lee-Tanghui/Vue-Testing-Demo

参考文章

测试框架 Mocha 实例教程 - 阮一峰
Chai.js断言库API中文文档
知乎: 如果对vue进行单元测试
Vue.js学习系列六——Vue单元测试Karma+Mocha学习笔记
单元测试
Element
前端单元测试之Karma环境搭建
前端自动化测试是干嘛的?
Karma官网

你可能感兴趣的:(前端,单元测试)