一次学会使用 mocha & jest 编写单元测试

单元测试是什么?

我原本打算先一条一条列出测试给咱们的前端项目带来的“先民血统”(保证项目的质量)和“劳动力解放”(自动化的力量)等诸多良好特性。但转念一想,您既然来了确定是知道测试的种种好处,至少您也确定知道 100% 覆盖率的测试真是亮瞎眼的装逼利器。前端

你认为测试难吗?node

我以为一点都不难,反而以为处处在复制和粘贴测试代码。这可不是代码缺乏复用性,我只是懒和测试根本没什么套路可言。这句话,并非说写测试像读网文小说同样没内涵,而是力求简洁直白git

若是你听过 TDD(测试驱动开发),对单元测试必定不会陌生,它是对一个模块、类或函数进行断言返回值的结果是否符合预期的结果。既然称为单元,也就是意味着咱们要进入每一个函数体,检查每行代码的执行状况,是一种精细的测试。es6

看到本教程的题目,您可能已经据说过 mocha 和 jest 的大名,也许您已经使用 mocha 写过一些测试。我之因此把长得跟一个妈生的两兄弟放在一块儿,就是由于他们实在太像了,既然要学,为何咱们不能一次掌握两个目前最火的单元测试框架呢。答案是,能够的。github

那么,咱们开始吧!正则表达式

第一个测试

特别声明:如下全部例子都已经过测试,还能够在 github/asVenus 查看本教程完整实例代码帮助您更好学习。npm

首先,经过 npm 安装单元测试框架 mocha 和 jest 与 chai 断言库json

$ cnpm i mocha chai jest -D
复制代码

咱们编写一个待测试的 sum 函数,它很是简单。api

// example/sum.js

module.exports = function sum (a, b) {
  return a + b
}
复制代码

而后,在 test 目录下编写咱们的第一个测试。数组

// test/sum.test.js

// 引入 chai 断言库
const expect = require('chai').expect
// 引入待测试的函数
const sum = require('../example/sum')

// describe 至关于一个测试的组,把一类相同的测试用例放在一块儿
// 第一个参数是对测试组的说明
// 第二个参数仅是一个普通的回调,咱们在里面放置一个或多个测试用例
describe('第一个测试',  () => {
  // it 就是一个测试用例
  it('sum', () => expect(sum(1, 2)).to.be.equal(3))
  // expect 接受一个 Actual,一个结果值
  // to.be 是 chai 的提升可读性的语言链(无关紧要)
  // equal 是一个断言函数,接受一个 Expected,一个期待值
  // 当 Actual 经过 equal 断言相等 Expected 时,测试经过
  // 反之,失败
})
复制代码

如今,就是如今,打开您的命令行,轻轻地敲下 mocha 这个单词,回车,mocha 会自动寻找到 test 文件并执行测试,你会看到。

image

是吧,咱们测试经过了,

如今咱们在 jest-test 目录下编写 jest 的测试。

// jest-test/sum.test.js

const sum = require('../example/sum')

describe('第一个测试',  ()  => {
  // jest 自带断言库
  it('sum', () => expect(sum(1, 2)).toBe(3))
})
复制代码

而后咱们在命令行敲下 jest jest-test,若是未发生意外你会看到。

image

若是你看到命令行抛出如下报错:
Cannot find module 'source-map-support' from 'source-map-support.js'
你还须要执行 npm i source-map-support -D 安装 jest 的依赖。而后再次执行便可。

那么,如今你应该能理解单元测试的本质了吧,就是断言,就是输入一个值而后根据一个断言的规则最后看是否符合期待值。

异步

前端业务中的异步场景多如牛毛,好比一个普通的回调、promise、监听事件、执行动画和接口调用等等。

mocha 和 jest 对于 promise 都有良好的支持,使测试更为轻松。

// example/getUserData.js

// 模拟一个获取用户数据的接口调用
module.exports = function getUserData() {
  return new Promise(resolve => {
    // 一秒后异步成功,返回一个 'ok'
    setTimeout(() => resolve('ok'), 1000)
  })
}
复制代码

mocha 和 jest 均可以直接将 promise 返回。

// mocha ☞ test/async.test.js

it('promise', () => {
  return getUserData()
    .then(data => {
      expect(data).to.be.equal('ok')
    })
})
复制代码
// jest ☞ jest-test/async.test.js

it('promise', () => {
  return getUserData()
    .then(data => {
      expect(data).toBe('ok')
    })
})
复制代码

另外,jest 还能够经过 resolves/rejects 修饰符直接测试 promise 的成功或失败状态。

// jest ☞ jest-test/async.test.js

it('promise2', () => {
  return expect(getUserData()).resolves.toBe('ok')
})

复制代码

mocha 和 jest 的 async/await 用法。

// mocha ☞ test/async.test.js

it('sync', async () => {
  expect(await getUserData()).to.be.equal('ok')
})
复制代码
// jest ☞ jest-test/async.test.js

it('async', async () => {
  expect(await getUserData()).toBe('ok')
})
复制代码

对于普通的异步测试(好比一个定时器),咱们须要手动使用 done 函数通知测试结束。

// example/timer.js

module.exports = function timer(fn) {
  setTimeout(fn, 1000)
}
复制代码
// mocha ☞ test/async.test.js

it('done',  done => {
  timer(() => {
    // 告诉 mocha 测试已经结束了!
    // 注意,mocha 只会等待 2s
    // 超时后,自动判断为测试失败
    done()
  })
})
复制代码
// jest ☞ jest-test/async.test.js

it('done',  done => {
  timer(() => {
    // jest 等待为 5s
    done()
  })
})
复制代码

钩子

mocha 和 jest 拥有相同的钩子机制,就连钩子的名字也相同。

// 全部测试执行前触发,只触发一次
beforeAll()
// 全部测试执行结束后触发,只触发一次
afterAll()
// 在每一个测试执行前触发
beforEach()
// 在每一个测试执行结束后触发
afterEach()
复制代码

要体现钩子在测试中的重要地位,我简单看一个测试的基本原则,就是每一个测试应当保持相互独立。也就是说,当咱们测试一个复杂类的各类状况时,类内部拥有本身的状态。当一个测试结束时(改变了类的内部状态),咱们忘记将其重置,在下一个测试中咱们很容易感到困惑(测试看起来应该是经过的可是却失败了)。由于,咱们产生了一个隐式的变化源,这将使得咱们须要额外花费精力记住每次测试后状态的改变,这很容易让测试变得困难并出错。正确的作法应该是在 beforeEach 钩子中从新 new 一个新的实例以重置状态。

// example/car.js

module.exports = class Car {
  constructor () {
    this.oilMass  = 10
  }

  start (mileage) {
    this.oilMass = mileage * .1
  }

  addOil (rise) {
    this.oilMass += rise
  }

}
复制代码
// jest ☞ jest-test/mock.js

const Car = require('../example/car')

describe('mock',  ()  =>{
  let car

  beforeEach(() => car = new Car())

  it('行驶', () => {
    car.start(10)
    expect(car.oilMass).toBe(1)
  })

  it('加油', () => {
    car.addOil(1)
    expect(car.oilMass).toBe(11)
  })
})

// 另外,咱们还能够单独测试一个用例
// 而不用担忧受到其余测试的限制
复制代码

钩子和一个 it 测试单例没什么区别,你也能够返回一个 promise 或使用 done 函数把同步的钩子变为异步的钩子。另外还有一点,钩子是具备做用域,当你放到 describe(测试组)内,仅对组内的全部测试用例有效,当放到外面时将会当前文件中全部的测试有效。

// 对全部测试有效
beforeEach()

describe('测试组一', () => {
  // 仅对测试一,测试二有效
  afterEach()    
  
  it('测试一')
  it('测试一')
})

describe('测试组二', () => {
  it('测试三')
})
复制代码

到此为止,mocha 和 jest 的基本使用讲完了,很轻松,对吧!我想您必定充满信心。那么,咱们再学一些具备挑战的东西,它们各自的“高级”特性。

jest mock

jest 的 mock,简而言之,就是各类模拟,好比 Function(函数)、Timer(定时器) 等等。

mock function

咱们先看一下,mock function 的具体使用。

若是咱们的测试函数接受一个回调函数,这个回调函数在内部被调用或进一步传递,而这个过程,咱们根本无力进行测试。可是经过 mock function 模拟一个 fn 做为测试的回调函数,咱们就有能力进行各类测试,好比测试 fn 的参数的个数、参数的值、是否被调用、调用次数以及调用的返回值等等。

// example/callback.js

module.exports = function callback (fn) {
  return fn(1, 2)
}
复制代码
// jest-test/mock.test.js

const callback = require('../example/callback')

describe('mock',  ()  =>{
  it('mock function', () => {
    // 建立一个 mock function
    const fn = jest.fn((a, b) => a + b)
    // 传入测试函数
    callback(fn)

    expect(fn).toHaveBeenCalled()          // 是否被调用
    expect(fn).toHaveBeenCalledTimes(1)    // 是否只调用了一次
    expect(fn).toHaveBeenCalledWith(1, 2)  // 参数值
    expect(fn).toHaveReturnedWith(3)       // 返回值

  })
})
复制代码

mock timer

原生的定时器函数(setTimeout, setInterval, clearTimeout, clearInterval)并非很方便测试,由于程序须要等待相应的延时。mock timer 经过覆盖原生定时器函数,可让您测试定时器是否被调用、传入的参数是不是函数以及等待的时间、甚至还能够控制时间流。

// example/timer.js

module.exports = function timer(fn) {
  setTimeout(fn, 1000)
}
复制代码
const timer = require('../example/timer')

// 让 jest 覆盖全局定时器并重置记录状态
beforeEach(() => jest.useFakeTimers())

it('mock timer', () => {
  // 建立一个 mock function 
  const fn = jest.fn()
  // 做为 timer 的回调函数
  timer(fn)

  // 检查 setTimeout 是否被调用了一次
  expect(setTimeout).toHaveBeenCalledTimes(1)
  // 检查 setTimeout 传入的两个参数
  // 是不是一个函数,是否要等待 1s
  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000)

  // 目前,传入到 timer 中的 fn 回调函数还没被调用
  expect(fn).not.toBeCalled()

  // 那么,咱们控制时间流
  // 让定时器立刻执行
  jest.runAllTimers()

  // 如今,fn 回调函数执行了!
  expect(fn).toBeCalled();
  expect(fn).toHaveBeenCalledTimes(1)
复制代码

mocha 万花筒

mocha 在测试报告的输入格式下了大功夫,提供给您足够多的选择,这一节更像是对命令行界面设计的展览。

在命令行执行如下命令,你将会看到不一样输入格式。

$ mocha --reporter 格式名
复制代码

spec(默认)- 分层规格列表

image

dot - 点矩阵

image

json - json 对象

image

progress - 进度条

image

list - 规格式列表

image

tap - 测试任何协议

image

landing - unicode 的起落跑道

image

min - 最少信息输入

image

nyan - 一只 nyan 喵!

image

markdown - markdown 文档 (github 口味)

你能够重定向为一个 md 文件

$ mocha --reporter markdown > test-reporter.md
复制代码

查看本教程生成的 test-reporter

jest 断言

jest 的断言风格和 chai 的 expect 相同。

expect(actual).toBe(expected)
复制代码

修饰符

修饰符用来限定断言的某种行为,放在断言函数或属性的前面。(只列出部分经常使用的修饰符)

  • not - 对断言取反
  • resolves - promise 的成功状态
  • rejects - promise 的失败状态
expect(true).not.toBe(false)
复制代码

匹配器

Jest 使用匹配器让你能够用各类方式测试你的代码。这里咱们介绍一些经常使用的匹配器快速开始您项目的测试。在 expect API 里能够查看到完整的列表。

基础

toBeNull 只匹配 null。

expect(null).toBeNull()
复制代码

toBeUndefined 只匹配 undefined。

expect(undefined).toBeUndefined()
复制代码

toBeDefined 与 toBeUndefined 相反。

expect(1).toBeDefined()
复制代码

toBeTruthy 匹配任何能够类型转换为 true 的值。

expect(true).toBeTruthy()
expect('sunny').toBeTruthy()
expect(1).toBeTruthy()
expect([]).toBeTruthy()
复制代码

toBeFalsy 匹配任何能够类型转换为 false 的值。

expect(false).toBeFalsy()
expect('').toBeFalsy()
expect(0).toBeFalsy()
复制代码

toBeNaN 只匹配 NaN。

expect(NaN).toBeNaN()
复制代码

toHaveLength 检查数组或字符串的 length

expect([1, 2, 3]).toHaveLength(3)
expect('abcd').toHaveLength(4)
复制代码

相等

toBe 使用 Object.js 方法进行相等比较。

expect(3).toBe(3)
expect(NaN).toBe(NaN)  // 经过
复制代码

toEqual 递归检查对象或数组的每一个字段。

expect({name: 'sunny', age: 22}).toEqual({age: 22, name: 'sunny'})
expect(['sunny', 22]).toEqual(['sunny', 22])
复制代码

数值

toBeGreaterThan 检查是否大于指定值。

expect(10).toBeGreaterThan(3)
复制代码

toBeGreaterThanOrEqual 检查是否大于等于指定值。

expect(10).toBeGreaterThanOrEqual(3)
expect(10).toBeGreaterThanOrEqual(10)
复制代码

toBeLessThan 检查是否小于指定值。

expect(10).toBeLessThan(20)
复制代码

toBeLessThanOrEqual 检查是否小于等于指定值。

expect(10).toBeLessThanOrEqual(20)
expect(10).toBeLessThanOrEqual(10)
复制代码

字符串

toMatch 使用正则表达式匹配字符串。

expect('mocha and jest').toMatch(/jest/)
复制代码

数组

toContain 检查一个数组或可迭代对象是否包含某项,还能够检查字符串是否包含每一个字符串。

expect([1, 2, 3]).toContain(3)
expect('mocha and jest').toContain('and')
复制代码

chai

chai 是一种支持多种风格(好比 expect 和 should)的断言库,咱们只介绍 expect。

const expect = require('chai').expect

expect(actual).to.be.equal(expected)
复制代码

语言链

语言链是单纯提供以提升断言的可读性,它们通常不提供测试功能(也就是无关紧要,写不写都行)

  • to
  • be
  • been
  • is
  • that
  • which
  • and
  • has
  • have
  • with
  • at
  • of
  • same

修饰符

  • not - 对断言取反
  • deep - 深度递归
  • length - 获取长度
expect(true).not.be.to.equal(false)
expect('abc').length.be.to.equal(3)
复制代码

断言

chai 部分断言和 jest 的匹配器使用上基本一致。这里简略地列出了一些 chai 经常使用的断言,以供您使用和查阅。在 Assert - Chai 能够查看到完整断言的列表。

// 是否为真值(转换为 true 的值)
ok
true
false
null
undefined
NaN
// 是否存在(即非 null 也非 undefined)
exist
// 是否为空
// 对于数组和字符串,它检查 length 属性
// 对于对象,它检查可枚举属性的数量
empty

// 断言值的类型
a/an(type)

// 严格相等(===)
equal(value)
// 至关于 deep.equal
eql(value)

// 数值相关的断言
above(value)           // 大于
below(value)           // 小于
most(value)            // 不大于
least(value)           // 不小于
within(start, finish)  // 闭合区间

// 对象拥有某个为名 name 的属性
property(name, [value])
// 启用 deep 修饰符后,还支持路径查询
deep.property('obj.a[1].c', 'sunny')

// 正则
match(regexp)
// 是否包含指定字符串
string(string)
复制代码

覆盖率测试

jest 集成了覆盖率测试,只须要在 babel.config.js 开启 collectCoverage 字段便可。

// jest.config.js

module.exports = {
  // 开启覆盖率测试
  collectCoverage: true,
  // 忽略的目录
  coveragePathIgnorePatterns: [
    'node_modules'
  ]
}
复制代码

而后,在命令行执行 jest jest-test 就会带上覆盖率测试的报告。因为,本教程的实例代码很简单,咱们已经达成了 100% 的装逼成就。覆盖率报告会详细的列出每次测试文件的

  • Stmts - 测试的有效代码行数
  • Bracnh - 代码分支
  • Funcs - 函数声明以及调用等
  • Lines - 测试执行到行级的状况
  • Uncovered Line #s - 当未到达 100% 时,会显示具体哪一行没测试到。

image

100% 覆盖率的测试的确闪眼,但一味追求覆盖率极可能会拔苗助长,极可能会改动代码而自"觉得聪明地"绕过 Uncovered Line 经过覆盖率测试,这种行为很是危险,会让测试质量变得不可靠,甚至使代码为了测试而编写。因此,不管任何状况下,覆盖率测试只能做为测试质量的一个参考标准,告诉咱们测试是否不够精确、哪里存在疏漏。这一点,咱们都应该铭记于心。

mocha 则须要第三方工具配合。

babel 支持

引入对 babel 的支持,可让我使用 es6 moduel 的导入方式和一些提案语法而且能够与基于 babel 作兼容的项目无缝衔接。

mocha:

$ npm i babel-core babel-preset-env babel-runtime -D
复制代码

在项目根目录下建立 babel 的配置文件 .babelrc

// .babelrc

{
  "presets": [ "env" ],
  "plugins": ["transform-runtime"]
}
复制代码

test 测试目录下建立 mocha 的配置文件 mocha.opts

# 测试报告输出的格式
--reporter tap
# 递归测试全部的目录和文件
--recursive
# 启动观察
# 只看文件发生改动自动从新启动测试
--watch
# 开启桌面通知
--growl
# 关键,让 mocha 支持 babel
--require babel-core/register
复制代码

注意,以上注释只是为了对每一个配置项进行说明。在您的配置文件中不能带有任何注释信息。

jest:

$ npm i babel-jest @babel/core @babel/preset-env -D
复制代码

在项目根目录下建立 babel 的配置文件 babel.config.js

// .babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}
复制代码

在根目录下打开命令行,执行 jest --init 命令生成 jest 的配置文件 jest.config.js

相关文章
相关标签/搜索