【大白话】单元测试

前言

对于大部分前端同窗们来讲,可能平时都没怎么接触过单元测试🙌,顶多在初始化 Vue 项目的时候看到过它问你要不要测试,或者据说过 karma、mocha 这些名词,但具体就不得而知了。其实这东西并不复杂,只是咱们没去学而已,它就像 Vue 同样容易上手,多写个几天,就可以像写 Vue 同样如鱼得水了🐠。javascript

为何要单元测试

因此,咱们为何须要单元测试呢🤔?
缘由很简单:就是为了减小 bug、提升产品稳定性,而不是为了测试而测试。对咱们开发来讲,它的好处也是显而易见的:就是保证代码质量。想一想咱们平时代码出问题的时候,是否是经常不敢去删除原有的代码,而是像打补丁同样往上加代码,主要缘由就是没有测试保障,你也不知道本身改了对不对、影响大不大💣。因此,若是有时间的话,单元测试仍是能够写写的。css

什么是单元测试

那么,什么是单元测试呢?简单来讲就是对(一些不常变更的)单元进行测试,对前端来讲你能够强行理解为😁:就是对一些通用函数和通用组件进行测试。再直白点说就是写一些测试代码来验证你的源代码是否符合预期,仅此而已。
正经点说,测试又可分为测试驱动开发(TDD)和行为驱动开发(BDD)两种,什么意思呢🤔?html

  • TDD:经过测试来推进整个开发的进行(就是测着测着出了 bug 就返回修改代码,注重测试结果)
  • BDD:经过行为来推进整个开发的进行(就是按着按着出了 bug 就返回修改代码,注重测试逻辑)

其实这些概念并不重要,咱们只要了解就行,毕竟这些概念也是近几年才出现,只是个称呼。
那既然单元测试是个不错的东西,为何大部分人都不写呢😅?说到这里,不得不说下单元测试最大的一个缺点:就是在一开始须要花不少时间。可是在大部分状况下,咱们不是在写需求就是在写需求的路上,没时间搞它,因此就不懂。与之矛盾的是它的优势:就是之后能够花更少的时间😯,尤为是若是你在开发新特性时,它能大大减小反作用。
ps: 单元测试的原则就是要尽可能独立和单一,这样才有利于测试、维护和理解。固然即便用例所有经过了也要通过人工测试,由于咱们不能保证集成在一块儿就不会有问题😬。前端

前置知识(关于测试工具)

这里先抛给你们一幅测试工具的关系图: vue

没看懂也不要紧,下面会讲解一波,但你要记住 karma 包含其余三个!!!

karma

karma 不是一个测试框架,也不是一个断言库,而是一个测试集成工具,它的主要做用就是集成其余各类测试工具(支持按需配置,你能够经过 karma 的配置文件来集成你喜欢的框架、断言库和浏览器等),而后自动打开浏览器运行你的测试脚本,测试结果一般会显示在命令行中。此外它还能够监听测试文件的变化,而后自执行。java

  • 总结:你能够粗浅的认为 karma 就是用来打开浏览器的。

mocha

mocha 是一个很经常使用的测试框架(相似的有 jasmine 和 jest 等),它既能够在 Node 中运行,也能够在浏览器中运行。它的主要做用是提供一些方便的语法来编写测试用例,以及对用例进行分组等。一个测试脚本能够由多个 descibe 组成,每一个 describe 又能够由多个 it 组成。descibe 主要就是用来分组,it 就是具体的测试用例代码。这里简要看下它的语法,以下:node

describe('分组一', () => {
    it('测试用例描述一', () => {})
    it('测试用例描述二', () => {})
})
describe('分组二', () => {
    it('测试用例描述一', () => {})
    it('测试用例描述二', () => {})
})
复制代码

这个就是固定写法,记住就行,没有什么为何👀。webpack

  • 总结:你能够粗浅的认为 mocha 就是用来编写测试用例的。

chai

由于 mocha 自己是不带断言的,因此须要和断言库结合使用。这里咱们选择 chai 这个断言库。它有三种不一样风格的写法,但意思是同样的,就像下面这样: git

这里咱们采用的是中间 expect 的写法,由于它比较符合天然语言(什么是天然语言?就是读起来比较顺)。而后举些例子🌰:

expect(1 + 1).to.be.equal(2); // 我期待 1 + 1 等于 2
expect('hello').to.be.a('string'); // 我期待 'hello' 是个字符串
expect('').to.be.empty; // 我期待 '' 是个空值
expect({ a: 1 }).to.have.property('a'); // 我期待 { a: 1 } 有一个属性 a
复制代码

要注意的是 chai 断言库中,to be been is has have 等这些词是没有意义的,只是为了读起来比较顺而已,事实上读起来也确实顺,若是你懂点基础英语的话。github

  • 总结:chai 是一个语义化的断言库

sinon

sinon 是一个测试辅助工具,它的本质工做是测试替身,也就是用来替换测试中的部分代码,使测试代码变得简洁。好比咱们要测一个函数是否被调用过,就能够借助 sinon.fake() 来实现,这是一个特殊的函数,如今不懂不要紧,用的时候你就知道了。

  • 总结:sinon 是一个测试辅助工具

以上就是单元测试所需用到的大部分工具知识,若是你们想要加深了解的话,能够自行百度。

开始实践

虽然花了这么大篇幅扯了这么久🌚,但上面的背景知识对咱们的理解是颇有帮助的。不过,好记性不如写代码,下面就让咱们赶忙撸起来吧💪。

初始化项目

先用 vue-cli 快速生成一个最简版的 Vue 项目,这里咱们选择 default。

安装各类依赖

要安装的依赖有点多,我就不详细说每一个东西是干吗的了,装就是了。

yarn add karma karma-chai karma-chai-spies karma-chrome-launcher karma-mocha karma-sinon-chai mocha chai sinon sinon-chai karma-webpack vue-loader -D
复制代码

新建 karma.conf.js 配置文件

执行 ./node_modules/karma/bin/karma init 命令,一路回车,就会在根目录生成一个 karma.conf.js 配置文件。 而后对这个文件作点修改,代码以下:

const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = function(config) {
    config.set({
        frameworks: ['mocha', 'sinon-chai', 'chai'], // 这是配置依赖包,karma 会自动引入这些包,后续咱们就不须要 import 了
        files: [
            'test/**/*.test.js', // 这是要执行的测试代码
        ],
        preprocessors: { // 这是在测试以前要先用 webpack 处理一下
            "src/**/*.*": ["webpack"],
            "test/**/*.test.js": ["webpack"]
        },
        webpack: {
            mode: 'development',
            module: {
                rules: [{
                    test: /\.js$/,
                    exclude: /(node_modules)/,
                    use: [{ loader: 'babel-loader'}]
                },
                {
                    test: /\.vue$/,
                    loader: 'vue-loader'
                }]
            },
            plugins: [
                new VueLoaderPlugin()
            ]
        }
    })
})
复制代码

顺便在根目录下新建一个空的 test 目录。
再顺便在 package.json 里面加上一个脚本命令 "test": "karma start --single-run"
最后的目录结构大体以下:

对函数进行测试

ok,接下来让咱们热个身,写个函数的测试用例。

写一个简单的函数

在 src 目录下新建一个 utils.js 文件,其内容以下:

// utils.js
function add(a, b) {
    return a + b
}
function multiply(a, b) {
    return a * b
}
export {
    add,
    multiply
}
复制代码

编写函数的测试用例

通常来讲测试文件名和源码文件名是一致的,因此咱们在 test 目录下新建一个 utils.test.js 文件。

import { add, multiply } from '../src/utils'

describe('工具函数测试', function() {
    it('求和函数测试', function() {
        let res = add(1, 1)
        expect(res).to.be.equal(2)
    })
    it('乘法函数测试', function() {
        let res = multiply(1, 1)
        expect(res).to.be.equal(1)
    })
})
复制代码

嗯,就这样,函数用例就编写完了,固然你也能够写的再复杂点。

运行函数的测试用例

咱们直接运行 yarn test 就可以看到以下结果:

能够看到咱们的两个用例都经过了,也许你会问道我怎么知道它有没有运行呢,很简单,你能够把 equal 里面的值故意改为错的运行一下,形如这样: expect(res).to.be.equal(100),你将会获得以下结果:
能够看到它会给你明显的错误提示。固然你也能够在 expect 后面打个 log 证实它执行了,以上就是函数的测试方法,是否是 easy 啊✌。
ps: 咱们执行 yarn test 就是执行 karma start --single-run,karma 会根据 karma.conf.js 的配置内容来执行 test 目录下的代码,并自动打开浏览器测试,结束后又自动关闭浏览器(--single-run 的做用),若是有报错就会打印在控制台中。

对组件进行测试

接下来咱们来看看 vue 组件是怎么测试的吧。首先,固然须要一个组件啦。

写一个简单的组件

在 src 下面新建一个简单的 demo.vue 组件,就像下面这样:

<!-- demo.vue -->
<template>
    <div class="demo" :class="isError ? 'demo--error' : ''" @click="$emit('click')">
        <span class="text" :style="`opacity: ${opacity}`" :data-msg="msg">哈哈哈</span>
        <slot></slot>
    </div>
</template>
<script> export default { name: 'Demo', props: { msg: { type: String, default: '' }, isError: { type: Boolean, default: false }, opacity: { type: [String, Number], default: 1 } } } </script>
复制代码

编写组件的基础测试用例

在 test 目录下新建 demo.test.js 文件,内容以下:

import Vue from 'vue/dist/vue.common.js'
import Demo from '../src/demo.vue'

Vue.config.productionTip = false
Vue.config.devtools = false

describe('Demo 组件测试', () => {
    it('存在', () => { // 首先得确保有 demo 这个东西
        expect(Demo).to.exist // 不是 undefined、null、0、''等 fasly 值就是 exist
    })
    describe('Demo 组件的基础功能测试', () => {
        it('.text 的文本内容测试', () => {
            const Constructor = Vue.extend(Demo)
            const vm = new Constructor().$mount() // 实例化组件
            console.log(vm.$el)
            expect(vm.$el.querySelector('.text').textContent).to.equal('哈哈哈') // 我期待 .text 元素的文本内容为 '哈哈哈'
        })
    })
})
复制代码

代码应该还算通俗易懂,其实测试用例的思路大致是一致的,主要核心思想就是:先实例化组件,而后用选择相应元素的一些可参照的东西进行断言,看看是否和预期相匹配。
ok,让咱们运行 yarn test 看下效果:

显然这个用例也是 ok 的。那如何知道错了呢,同以前的函数同样,也故意把 equal('哈哈哈') 改错就行,以后就再也不赘述了,就像下面这样:

编写组件的 props 测试用例

咱们直接上代码,你们应该都能读懂,写法是同样样的😎:

// ...
describe('Demo 组件测试', () => {
    describe('Demo 组件的基础功能测试', () => {})
    describe('Demo 组件的 props 测试', () => {
        it('.text 的属性值为黄小芮', () => { // 测试标签属性
            const Constructor = Vue.extend(Demo)
            const vm = new Constructor({
                propsData: { // 这是传参的固定写法,没必要纠结
                    msg: '黄小芮'
                }
            }).$mount()
            expect(vm.$el.querySelector('.text').getAttribute('data-msg')).to.equal('黄小芮') // 我期待 .text 元素的 data-msg 属性值为 '黄小芮'
        })
        
        it('.demo 是否有 demo--error 的样式名', () => { // 测试样式名
            const Constructor = Vue.extend(Demo)
            const vm = new Constructor({
                propsData: {
                    isError: true
                }
            }).$mount()
            expect(vm.$el.classList.contains('demo--error')).to.equal(true) // 我期待 vm.$el 的样式列表包含 demo--error 样式名
        })
        
        it('.text 的 opacity 样式', () => { // 测试 css 样式(放到页面中才会有样式)
            const div = document.createElement('div')
            document.body.appendChild(div)
            const Constructor = Vue.extend(Demo)
            const vm = new Constructor({
                propsData: {
                    opacity: 0.5
                }
            }).$mount(div)
            const ele = vm.$el.querySelector('.text')
            expect(getComputedStyle(ele).opacity).to.equal('0.5') // 我期待 .text 元素的 css 样式 opacity 值为 '0.5',注意这里是字符串,css 的属性值都是字符串
        })
    })
})
复制代码

编写组件的 slot 测试用例

这里也直接上代码,要注意的是 slot 和上面实例化组件的方法有点不太同样:

// ...
describe('Demo 组件测试', () => {
    describe('Demo 组件的基础功能测试', () => {})
    describe('Demo 组件的 props 测试', () => {})
    describe('Demo 组件的 slot 测试', () => {
        it('slot 测试', (done) => { // 异步函数须要加 done 参数说明一下,也是固定写法
            Vue.component('xr-demo', Demo)
            let div = document.createElement('div')
            document.body.appendChild(div)
            // 这边咱们的写法和上面的不太同样,不是经过 new 来实例化,而是直接写 html
            div.innerHTML = ` <xr-demo> <p id="xr"></p> </xr-demo> `
            const vm = new Vue({
                el: div
            })
            setTimeout(() => { // 这是个异步的过程,通常用 $nextTick 和 setTimeout 处理
                let p = vm.$el.querySelector('#xr')
                expect(p).to.exist // 咱们期待在组件中能找到 id 为 xr 的元素
                done() // 异步函数后面须要调用一下 done(),也是固定写法
            })
        })
    })
})
复制代码

编写组件的 event 测试用例

这里以 click 事件为例子🌰,那么如何测试点击事件呢?咱们知道点击事件无非就是要执行一个函数,只要函数被调用了就说明点击事件发生了,那么怎么证实一个函数被执行了呢🤔????嗯,是个大问题,因此,咱们须要用前面说过的 sinon.fake() 来打辅助,具体怎么写,仍是直接上代码:

// ...
describe('Demo 组件测试', () => {
    describe('Demo 组件的基础功能测试', () => {})
    describe('Demo 组件的 props 测试', () => {})
    describe('Demo 组件的 slot 测试', () => {})
    describe('Demo 组件的 event 测试', () => {
        it('Demo 上的 click 事件', () => {
            const Constructor = Vue.extend(Demo)
            const vm = new Constructor().$mount()
            const callback = sinon.fake(); // 这是 sinon 的特有函数
            vm.$on('click', callback) // 添加事件监听
            vm.$el.click() // 点击组件,会触发上面👆那行的监听,从而触发 callback
            expect(callback).to.have.been.called // 咱们期待 callback 被调用过
        })
    })
})
复制代码

这个东西也是固定的套路,多写就会了,就像 Vue 同样。

一些问题

有点重复

假如你写了一遍上面的那些测试用例,你会发现代码好像有点重复,有点重复就说明咱们能够优化它,因而就要说到 mocha 的几个钩子函数(这里只大概描述一下):

describe('hooks', function() {
  before(function() {
    // runs before all tests in this block
  });
  after(function() {
    // runs after all tests in this block
  });
  beforeEach(function() {
    // runs before each test in this block
  });
  afterEach(function() {
    // runs after each test in this block
  });
  // test cases
  it('case one', () => {})
  it('case two', () => {})
});
复制代码

也就是说咱们在执行 it 以前会先调用 beforeEach 这个钩子,执行 it 以后调用 afterEach 这个钩子。这样一来咱们就能够把实例化组件的代码抽离出来写在 beforeEach 里面。

没有及时销毁

另外,你可能还注意到,咱们的实例没有及时销毁,因此咱们也能够在 afterEach 这个钩子里面作相应的处理,就像下面这样:

afterEach(function() {
    // 移除元素并释放内存
    vm.$el.remove()
    vm.$destroy()
});
复制代码

每次修改都要手动执行脚本

咱们可不能够保存的时候就自动执行 yarn test 呢。嗯,是能够的,小小修改一下最初的脚本命令就行,就像这样:"test": "karma start",这下咱们保存的时候它就会自动测试一遍了。

别人家的单元测试

👌,接下来就是见证奇迹的时刻😊。如今让咱们打开 Element 的源码来看看别人的单元测试是怎么写的(瞟一眼就行):

有没有发现,你忽然一下变的牛逼了,之前看不懂的东西,如今刚接触就看懂了。嗯,是的,单元测试不过如此,要是再多写个几天就能够信手拈来了(其实坑仍是有的😂,可是多写就行了,都是套路)。另外,Vue 自己集成了整套的测试流程,官网上也有对应的文档示例,具体写法会有所不一样,但思想大同小异,咱们只要照着写个两三遍就会了。

结语

因此,最终咱们要怎么应用到实际工做中呢?em...我想大部分公司的后台管理系统应该是一个施展才华的好地方。至于覆盖率,多写多覆盖罗,对于大部分前端同窗来讲没必要太较真,毕竟咱们是要写需求的啊。最后的最后,其实不少东西都不难,只是咱们没碰触过因此总以为高不可攀。常言道会者不难,难者不会,说的就是这个道理(大赞无疆👍👍👍。。。)。

ps: 若有须要上述代码的请点击这里: 单元测试 demo 传送门 ps: 后面我会每个月写篇【大白话】系列文章,用通俗的语言让你看了就懂,欢迎关注。

相关文章
相关标签/搜索