前端如何进行单元测试?

38次阅读

共计 10780 个字符,预计需要花费 27 分钟才能阅读完成。

Mocha
Mocha 是 JavaScript 的测试框架, 浏览器和 Node 端均可以使用。但是 Mocha 本身并不提供断言的功能, 需要借助例如: Chai, 这样的断言库完成测试的功能。
Mocha API 速览
Mocha 的 API 快速浏览, 更多细节请参考文档

⚠️ 注意:

Mocha 不推荐使用箭头函数作为 Callback 的函数
不要用例中什么都不做, 推荐使用 skip 跳过不需要的测试

Mocha 简单的示例
describe(‘unit’, function () {
it(‘example’, function () {
return true
})
})
Mocha 测试异步代码
Mocha 支持 Promise, Async, callback 的形式

// callback
describe(‘ 异步测试 Callback’, function () {
it(‘Done 用例 ’, function (done) {
setTimeout(() => {
done()
}, 1000)
})
})

// promise
describe(‘ 异步测试 Promise’, function () {
it(‘Promise 用例 ’, function () {
return new Promise((resolve, reject) => {
resolve(true)
})
})
})

// async
describe(‘ 异步测试 Async’, function () {
it(‘Async 用例 ’, async function () {
return await Promise.resolve()
})
})
钩子

before, 全部的测试用例之前执行
after, 全部的测试用例结束后执行
beforeEach, 每一个测试用例前执行
afterEach, 每一个测试用例后执行

// before, beforeEach, 1, afterEach, beforeEach, 2, afterEach, after
describe(‘MochaHook’, function () {
before(function () {
console.log(‘before’)
})

after(function () {
console.log(‘after’)
})

beforeEach(function () {
console.log(‘beforeEach’)
})

afterEach(function () {
console.log(‘afterEach’)
})

it(‘example1’, function () {
console.log(1)
})

it(‘example2’, function () {
console.log(2)
})
})
异步钩子
Mocha Hook 可以是异步的函数, 支持 done,promise, async
全局钩子(root hook)
如果 beforeEach 在任何 descride 之外添加, 那么这个 beforeEach 将被视为 root hook。beforeEach 将会在任何文件, 任何的测试用例前执行。

beforeEach(function () {
console.log(‘root beforeEach’)
})

describe(‘unit1’, function () {
//…
})
DELAYED ROOT SUITE
如果需要在任何测试用例前执行异步操作也可以使用 (DELAYED ROOT SUITE)。使用 ”mocha –delay” 执行测试脚本。”mocha –delay” 会添加一个特殊的函数 run() 到全局的上下文。当异步操作完成后, 执行 run 函数可以开始执行测试用例

function deplay() {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve()
}, 1000)
})
}

deplay().then(function () {
// 异步操作完成后, 开始执行测试
run()
})

describe(‘unit’, function () {
it(‘example’, function () {
return true
})
})
skip
describe, 或者 it 之后添加 skip。可以让 Mocha 忽略测试单元或者测试用例。使用 skip, 测试会标记为待处理。
重试测试
设置测试失败后, 测试重试的次数

describe(‘retries’, function () {
it(‘retries’, function () {
// 设置测试的重试次数
this.retries(3)
const number = Math.random()
if (number > 0.5) throw new Error()
else return true
})
})
动态生成测试
对于一些接口的测试, 可以使用动态生产测试用例, 配置请求参数的数组, 循环数组动态生成测试用例

describe(‘ 动态生成测试用例 ’, function () {
let result = []
for(let i = 0; i < 10; i ++) {
result.push(Math.random())
}
result.forEach((r, i) => {
// 动态生成测试用例
it(` 测试用例 ${i + 1}`, function () {
return r < 1
})
})
})

slow
如果测试用例, 运行时间超过了 slow 设置的时间, 会被标红。

describe(‘unit’, function () {
it(‘example’, function (done) {
this.slow(100)
setTimeout(() => {
done()
}, 200)
})
})
timeout
设置测试用例的最大超时时间, 如果执行时间超过了最大超时时间,测试结果将为错误

describe(‘unit’, function () {
it(‘example’, function (done) {
this.timeout(100)
setTimeout(() => {
done()
}, 200)
})
})
Chai
Chai 是 Node 和浏览器的 BDD/TDD 断言库。下面将介绍, BDD 风格的 API, expect。should 兼容性较差。
Chai API 速览
Chai 的 API 快速浏览, 更多细节请参考文档

not
对断言结果取反

it(‘not’, function () {
const foo = 1
expect(foo).to.equal(1)
expect(foo).to.not.equal(2)
})
deep
进行断言比较的时候, 将进行深比较

it(‘deep’, function () {
const foo = [1, 2, 3]
const bar = [1, 2, 3]
expect(foo).to.deep.equal(bar)
expect(foo).to.not.equal(bar)
})
nested
启用点和括号表示法

it(‘nested’, function () {
const foo = {
a: [
{
a: 1
}
]
}
expect(foo.a[0].a).to.equals(1)
})
own
断言时将会忽略对象 prototype 上的属性

it(‘own’, function () {
Object.prototype.own = 1
const a = {}
expect(a).to.not.have.own.property(‘own’)
expect(a).to.have.property(‘own’)
})
ordered
ordered.members 比较数组的顺序是否一致, 使用 include.ordered.members 可以进行部分比较, 配合 deep 则可以进行深比较

it(‘order’, function () {
const foo = [1, 2, 3]
const bar = [1, 2, 3]
const faz = [1, 2]
const baz = [{a: 1}, {b: 2}]
const fzo = [{a: 1}, {b: 2}]
expect(baz).to.have.deep.ordered.members(fzo)
expect(foo).to.have.ordered.members(bar)
expect(foo).to.have.include.ordered.members(faz)
})
any
要求对象至少包含一个给定的属性

it(‘any’, function () {
const foo = {a: 1, b: 2}
expect(foo).to.have.any.keys(‘a’, ‘b’, ‘c’)
})
all
要求对象包含全部给定的属性

it(‘all’, function () {
const foo = {a: 1, b: 2}
expect(foo).to.not.have.all.keys(‘a’, ‘c’)
})
a, an
用来断言数据类型。推荐在进行更多的断言操作前, 首先进行类型的判断

it(‘a’, function () {
expect(‘123’).to.a(‘string’)
expect(false).to.a(‘boolean’)
expect({a: 1}).to.a(‘object’)
expect(‘123’).to.an(‘string’)
expect(false).to.an(‘boolean’)
expect({a: 1}).to.an(‘object’)
})
include
include 断言是否包含, include 可以字符串, 数组以及对象(key: value 的形式)。同时可以配合 deep 进行深度比较。

it(‘include’, function () {
expect(‘love fangfang’).to.an(‘string’).to.have.include(‘fangfang’)
expect([‘foo’, ‘bar’]).to.an(‘array’).to.have.include(‘foo’, ‘bar’)
expect({a: 1, b: 2, c: 3}).to.an(‘object’).to.have.include({a: 1})
expect({a: {b: 1}}).to.an(‘object’).to.deep.have.include({a: {b: 1}})
expect({a: {b: [1, 2]}}).to.an(‘object’).to.nested.deep.have.include({‘a.b[0]’: 1})
})
ok, true, false, null, undefined, NaN

ok 断言, 类似 ”==”
true 断言, 类似 ”===”
flase 断言, 与 false 进行 ”===” 比较
null 断言, 与 null 进行 ”===” 比较
undefined 断言, 与 undefined 进行 ”===” 比较
NaN 断言, 与 NaN 进行 ”===” 比较

empty
断言数组, 字符串长度为空。或者对象的可枚举的属性数组长度为 0
equal
进行 ”===” 比较的断言
eql
可以不使用 deep 进行严格类型的比较
above, least, below, most

above 大于断言
least 大于等于断言
below 小于断言
most 小于等于断言

within
范围断言

it(‘within’, function () {
expect(2).to.within(1, 4)
})
property
断言目标是否包含指定的属性

it(‘property’, function () {
expect({a: 1}).to.have.property(‘a’)
// property 可以同时对 key, value 断言
expect({a: { b: 1}}).to.deep.have.property(‘a’, {b: 1})
expect({a: 1}).to.have.property(‘a’).to.an(‘number’)
})
lengthOf
断言数组, 字符串的长度

it(‘lengthOf’, function () {
expect([1, 2, 3]).to.lengthOf(3)
expect(‘test’).to.lengthOf(4)
})
keys
断言目标是否具有指定的 key

it(‘keys’, function () {
expect({a: 1}).to.have.any.keys(‘a’, ‘b’)
expect({a: 1, b: 2}).to.have.any.keys({a: 1})
})
respondTo
断言目标是否具有指定的方法

it(‘respondTo’, function () {
class Foo {
getName () {
return ‘fangfang’
}
}
expect(new Foo()).to.have.respondsTo(‘getName’)
expect({a: 1, b: function () {}}).to.have.respondsTo(‘b’)
})
satisfy
断言函数的返回值为 true, expect 的参数是函数的参数

it(‘satisfy’, function () {
function bar (n) {
return n > 0
}
expect(1).to.satisfy(bar)
})
oneOf
断言目标是否为数组的成员

it(‘oneOf’, function () {
expect(1).to.oneOf([2, 3, 1])
})
change
断言函数执行完成后。函数的返回值发生变化

it(‘change’, function () {
let a = 0

function add () {
a += 3
}

function getA () {
return a
}

// 断言 getA 的返回值发生变化
expect(add).to.change(getA)
// 断言变化的大小
expect(add).to.change(getA).by(3)
})

Karma
什么是 Karma?
Karma 是一个测试工具,能让你的代码在浏览器环境下测试。代码可能是设计在浏览器端执行的,在 node 环境下测试可能有些 bug 暴露不出来;另外,浏览器有兼容问题,karma 提供了手段让你的代码自动在多个浏览器(chrome,firefox,ie 等)环境下运行。如果你的代码只会运行在 node 端,那么你不需要用 karma。
安装, 运行 Karma

// 安装 karma
npm install karma –save-dev

// 在 package.json 文件中添加测试命令
scripts: {
test:unit: “karma start”
}
通过 Karma 测试项目, 需要在项目中添加配置 karam.conf.js 的文件。推荐使用 karam init 命令生成初始化的配置文件。下面是, karam init 命令的配置项。生成配置文件之后, 就可以通过 ”npm run test:unit” 命令进行单元测试了。

1. Which testing framework do you want to use(使用的测试框架)?
mocha
2. Do you want to use Require.js(是否使用 Require)?
no
3. Do you want to capture any browsers automatically(需要测试的浏览器)?
Chrome, IE,
4. What is the location of your source and test files(测试文件的位置)?
test/*.test.js
5. Should any of the files included by the previous patterns be excluded(需要排除的文件) ?
node_modules
6. Do you want Karma to watch all the files and run the tests on change(什么时候开始测试) ?
change
添加断言库

// 安装
npm install –save-dev chai karma-chai
配置 karma.conf.js 与 webpack
如果测试发送在浏览器环境, Karma 会将测试文件, 模拟运行在浏览器环境中。所以推荐使用 webpack, babel, 对测试文件进行编译操作。Karma 中提供了处理文件中间件的配置。ps: 之前由于浏览器环境不支持 require, 而我在 test 文件中使用了 require, 并且我没有将测试文件进行编译, 耽误了我半天的时间:(
karma.conf.js 配置的更多的细节,可以查看 karma 文档

// 安装 babel
npm install –save-dev karma-webpack webpack babel-core babel-loader babel-preset-env

// 对文件添加 webpack 的配置, 对配置文件使用 babel 进行处理
module.exports = function(config) {
config.set({

basePath: ”,

frameworks: [‘mocha’, ‘chai’],

files: [
‘test/*.test.js’
],

exclude: [
‘node_modules’
],

preprocessors: {
‘test/*.test.js’: [‘webpack’]
},

webpack: {
// webpack4 中新增的 mode 模式
mode: “development”,

module: {
rules: [
{test: /\.js?$/, loader: “babel-loader”, options: { presets: [“env”] }, },
]
}
},

reporters: [‘progress’],

port: 9876,

colors: true,

logLevel: config.LOG_INFO,

autoWatch: true,

browsers: [‘Chrome’],

singleRun: false,

concurrency: Infinity
})
}
编写测试文件

// 通过 babel, 浏览器可以正常的解析测试文件中的 require
const modeA = require(‘../lib/a’)
const expect = require(‘chai’).expect

describe(‘test’, function () {
it(‘example’, function () {
expect(modeA.a).to.equals(1)
})
})
Vue 与 Karma 集成
与处理浏览器中的 require 同理, 如果我们需要对.vue 文件进行测试, 则需要通过 vue-loader 的对.vue 文件进行处理。
我们首先通过 vue-cli 初始化我们的项目, 这里我使用的是 vue-cli2.x 的版本, 3.x 的版本 vue-cli 对 webpack 的配置作出了抽象, 没有将 webpack 的配置暴露出来, 我们会很难理解配置。如果需要使用 vue-cli3.x 集成 karma, 则需要另外的操作。

// 安装 karma 以及 karam 的相关插件
npm install karma mocha karma-mocha chai karma-chai karma-webpack –save-dev

// 配置 karma.conf.js
// webpack 的配置直接使用 webpack 暴露的配置
const webpackConfig = require(‘./build/webpack.test.conf’)

module.exports = function(config) {
config.set({

basePath: ”,

frameworks: [‘mocha’],

files: [
‘test/*.test.js’
],

exclude: [
],

// 测试文件添加中间件处理
preprocessors: {
‘test/*.test.js’: [‘webpack’]
},

webpack: webpackConfig,

reporters: [‘progress’],

port: 9876,

colors: true,

logLevel: config.LOG_INFO,

autoWatch: true,

browsers: [‘Chrome’],

singleRun: false,

concurrency: Infinity
})
}

⚠️ 注意: 这里依然存在一个问题 Can’t find variable: webpackJsonp, 我们可以将 webpackConfig 文件中的 CommonsChunkPlugin 插件注释后, karma 将会正常的工作。
编写测试
import {expect} from ‘chai’
import {mount} from ‘@vue/test-utils’
import HelloWorld from ‘../src/components/HelloWorld.vue’

describe(‘HelloWorld.vue’, function () {
const wrapper = mount(Counter)
it(‘Welcome to Your Vue.js App’, function () {
console.log(‘Welcome to Your Vue.js App’)
expect(wrapper.vm.msg).to.equals(‘Welcome to Your Vue.js App’)
})
})
Vue-cli3 与 Karma 集成
对于 vue-cli3 我尝试自己添加 karma.conf.js 的配置, 虽然可以运行,但是存在问题。issue 中, 官方建议我在 vue-cli3 版本中使用 vue-cli 的 karma 的插件解决。
对于 vue-cli3,可以使用 vue-cli-plugin-unit-karma 插件, 集成 vue-cli3 与 karma
VueTestUtils
对于 VueTestUtils, 我这里并不想做过多的介绍。因为它拥有详尽和完善的中文文档, 在这里我只会做大致的概述。文档地址, 值得注意的一点文档中部分内容已经过时, 以及不适用与 vue-cli3

什么是 VueTestUtils?
VueTestUtils 是 Vue.js 官方的单元测试实用工具库, 提供很多便捷的接口, 比如挂载组件, 设置 Props, 发送 emit 事件等操作。我们首先使用 vue-cli3 创建项目, 并添加 vue-cli-plugin-unit-karma 的插件。而 vue-cli-plugin-unit-karma 插件已经集成了 VueTestUtils 工具, 无需重复的安装。
VueRouter
VueRouter, 是 Vue 的全局插件, 而我们测试的都是单文件组件, 我们该如何测试 VueRouter 的呢?, VueTestUtils 为我们提供了 localVue 的 API, 可以让我们在测试单文件组件的时候, 使用 VueRouter。(更多内容请参考文档)

import {mount, createLocalVue} from ‘@vue/test-utils’
import Test from ‘../../src/views/Test.vue’
import VueRouter from ‘vue-router’

it(‘localVue Router’, function () {
const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()
// 挂载组件的同时, 同时挂载 VueRouter
const wrapper = mount(Test, {
localVue,
router
})
// 我们可以在组件的实例中访问 $router 以及 $route
console.log(wrapper.vm.$route)
})
对于 $router 以及 $route, 我们也可以通过 mocks 进行伪造, 并注入到组件的实例中

it(‘mocks’, function () {
// 伪造的 $route 的对象
const $route = {
path: ‘/’,
hash: ”,
params: {id: ‘123’},
query: {q: ‘hello’}
}

const wrapper = mount(Test, {
mocks: {
$route
}
})
// 在组件的实例中访问伪造的 $route 对象
console.log(wrapper.vm.$route)
})
Vuex
对于 Vuex 的测试, 我们需要明确一点我们不关心这个 action 或者 mutation 做了什么或者这个 store 是什么样子的, 我们只需要测试 action 将会在适当的时机触发。对于 getters 我们也不关心它返回的是什么, 我们只需要测试这些 getters 是否被正确的渲染。更多细节请查看文档。

describe(‘Vuex’, function () {

// 应用全局的插件 Vuex
const localVue = createLocalVue()
localVue.use(Vuex)

let actions
let store
let getters
let isAction = false

// 在每个测试用例执行前, 伪造 action 以及 getters
// 每个测试用例执行前, 都会重置这些数据
beforeEach(function () {
// 是否执行了 action
isAction = false

actions = {
actionClick: function () {
isAction = true
},
actionInput: function () {
isAction = true
}
}

getters = {
name: () => ‘ 方方 ’
}

// 生成伪造的 store
store = new Vuex.Store({
state: {},
actions,
getters
})
})

// 测试是否触发了 actions
it(‘ 如果 text 等于 actionClick, 触发了 actionClick action’, function () {
const wrapper = mount(TestVuex, { store, localVue})
wrapper.vm.text = ‘actionClick’
// 如果成功触发 actionClick action, isAction 将为 true
expect(isAction).to.true
})

it(‘ 如果 text 等于 actionInput,触发 actionInput action’, function () {
const wrapper = mount(TestVuex, { store, localVue})
wrapper.vm.text = ‘actionInput’
// 如果成功触发 actionInput action, isAction 将为 true
expect(isAction).to.true
})

// 对于 getters, 同理 action
// 我们只关注是否正确渲染了 getters, 并不关心渲染了什么
it(‘ 测试 getters’, function () {
const wrapper = mount(TestVuex, {
store,
localVue
})
// 测试组件中使用了 getters 的 dom, 是否被正确的渲染
expect(wrapper.find(‘p’).text()).to.equals(getters.name())
})
})

正文完
 0