前端测试之 Jest 单元测试

1、Jest 简介

  1. 优点: 速度快、API简单、配置简单
  2. 前置: Jest 不支持 ES Module 语法,须要安装 babel
npm install -D @babel/core @babel/preset-env
复制代码

.babelrcjavascript

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

jest 在运行前会检查是否安装 babel,若是安装了会去取 .babelrc 文件,结合 babel 将代码进行转化,运行转化后的代码。 3. jest 默认配置css

npx jest --init
复制代码
  1. jest 模式
  • jest --watchAll:当发现测试文件变更,将全部测试文件从新跑一遍
  • jest --watch:须要和 git 结合使用,会比较现有文件和 commit 的文件的差别,只测试差别文件

2、Jest 匹配器

常见匹配器

  • toBe
  • toEqual:判断对象内容是否相等
  • toMatchObject:expect(obj).toMatchObject(o),指望 o 中包含 obj
  • toBeNull
  • toBeUndefined
  • toBeDefinded
  • toBeTruthy
  • toBeFalsy
  • not:用于否认,好比 .not.toBeTruthy()

Number 相关

  • toBeGreaterThan(大于) / toBeGreaterThanOrEqual(大于等于)
  • toBeCloseTo:用于比较浮点数,近似相等时断言成立
  • toBeLessThan / toBeLessThanOrEqual

String 相关

  • toMatch:参数能够传字符串或正则

Array Set 相关

  • toContain

异常匹配器

  • toThrow:
const throwError = () => {
  throw new Error('error')
}

it('can throw error', () => {
  expect(throwError).toThrow('error') // 判断throw函数能够抛出异常,异常信息为 "error"。也能够写正则
})
复制代码

这里有个小技巧:当咱们想忽略掉单个文件中的其余测试用例,只针对一个测试用例作调试的时候,能够加上 .onlyhtml

it.only('test', () => {
  // ...
})
复制代码

但这并不会忽略其余测试文件的测试用例前端

3、测试异步代码

这里有三个异步方法,对这三个方法进行代码测试,"www.dell-lee.com/react/api/d…" 会返回 {success: true}, "www.dell-lee.com/react/api/4…" 则不存在。vue

import axios from 'axios'

export function getData1() {
  return axios.get('http://www.dell-lee.com/react/api/demo.json')
}

export function getData2(fn) {
  axios.get('http://www.dell-lee.com/react/api/demo.json').then(res => {
    fn(res)
  })
}

export function get404() {
  return axios.get('http://www.dell-lee.com/react/api/404.json')
}
复制代码

对于异步代码测试,时机很重要,必须保证咱们的测试用例在异步代码走完以后才结束。有如下几种办法:java

  1. done,控制测试用例结束的时机
  2. 若是函数执行的返回值是 Promise,将这个 Promise return 出去
  3. async + await
import {getData1, getData2, get404} from './fetchData/fetchData'

it('getData1 方法1', (done) => {
  getData1().then(res => {
    expect(res.data).toEqual({
      success: true
    })
    done()  // 若是不加 done,还没执行到 .then 方法,测试用例已经结束了
  })
})

it('getData1 方法2', () => {
  return getData1().then(res => {
    expect(res.data).toEqual({
      success: true
    })
  })
})

it('getData2 方法2', (done) => {
  getData2((res) => {
    expect(res.data).toEqual({
      success: true
    })
    done()
  })
})

it('getData1 方法3', async () => {
  const res = await getData1()
  expect(res.data).toEqual({
    success: true
  })
})

/*********** 重点关注 ***********/
it('get404', (done) => {
  expect.assertions(1)
  get404().catch(r => {
    expect(r.toString()).toMatch('404')
    done()
  })
})
复制代码

重点讲一下上面的最后一个测试用例,假设咱们如今有一个返回的是 404 的接口,咱们须要对这个接口测试,指望他返回 404。 咱们用 catch 捕获,在 catch 中判断。node

可是,假如这个接口返回的不是 404,而是正常返回 200,这个 catch 则不会执行,expect 也不会执行,测试依然是经过的。这不符合咱们的预期!因此,咱们须要加上 expect.assertions(1) 进行断言:下面必定会执行一个 expectreact

固然,也能够用 async await 方法进行 404 接口的测试ios

it('get404 方法3', async () => {
  await expect(get404()).rejects.toThrow()
})
复制代码

4、Jest 中的一些钩子函数

  • beforeAll:全部用例开始执行前
  • beforeEach:每一个用例执行前
  • afterEach
  • afterAll
  • describe

前四个钩子使用起来很简单,调用方法以下:git

beforeAll(() => {
  // ...
})
复制代码

若是测试先后要作一些处理,尽量写在这些钩子函数中,他能保证必定的执行顺序。

describe 能够用来进行用例分组,为了让咱们的测试输出结果更好看,更有层次。 同时,在每一个 describe 中都有上面 4 个钩子函数的存在,咱们来看看具体的状况:

describe('测试 Button 组件', () => {
  beforeAll(...)  // 1
  beforeEach(...) // 2
  afterEach(...)  // 3
  afterAll(...)   // 4

  describe('测试 Button 组件的事件', () => {
    beforeAll(...)  // 5
    beforeEach(...) // 6
    afterEach(...)  // 7
    afterAll(...)   // 8
    it('event1', ()=>{...})
  })
})
复制代码

上面钩子函数的执行顺序是: 1 > 5 > 2 > 6 > 3 > 7 > 4 > 8
外部的钩子函数对 describe 内部的用例也生效,执行顺序为:先外部后内部

5、Jest 中的 mock

1. 在 Jest 中 mock 异步方法

前面提到了能够测试异步代码,对于一些接口都能进行请求测试。但假如每个接口都真的发起请求,那一次测试须要耗费的时间是不少的。 这时候咱们能够模拟请求方法,步骤以下:

  1. mock.js 中导出了咱们的请求方法
import axios from 'axios'

export function getData() {
  return axios.get('http://www.dell-lee.com/react/api/demo.json')
}
复制代码
  1. 在 mock.js 的同级目录下建一个 mocks 的文件夹,文件夹内创建对应文件名的文件,这个文件就是导出的方法就是模拟请求的方法
    1580732355(1)
    这里咱们直接返回一个 Promise,把假数据 resolve 出去
export function getData() {
  return Promise.resolve({
    success: true
  })
}
复制代码
  1. 测试用例部分: 这里有一个须要注意的jest.mock 不能写在任何钩子函数里,由于钩子函数的执行时机问题,beforeAll 也不行,当钩子函数执行时,没有写在钩子函数里面的代码已经执行了,也就是已经 import 了!
jest.mock('./mock/mock.js')  // 声明下面引入的 getData 方法是 jest 模拟的,若是不须要引入该方法则不须要声明

import {getData} from './mock/mock.js'  // 导入 mock.js,但实际上 jest 会导入 __mocks__ 下的 mock.js

test('mock 方法测试', () => {
  getData().then(data => {
    expect(data).toEqual({
      success: true
    })
    done()
  })
  
})
复制代码

除了上面的这种办法,还能在 jest.config.js 中配置自动开启 mock,这样 jest 会自动去查找当前文件同级有没有 mock 文件夹,里面有没有对应文件

module.exports = {
  automock: true
}
复制代码

讲了两种 mock 的方法,还有一种极端状况须要避免 mock:
咱们在 mock.js 中定义了一个须要 mock 的 getData 方法,又另外定义了一个不须要 mock 的普通方法,当咱们在测试文件导入的时候,须要避免 jest 去 mocks/mock.js 下找这个普通方法,这里须要用 jest 提供的方法导入:

const { regularMethod } = jest.requireActual('./mock/mock.js')
复制代码

2. 用 Jest 操控时间

当咱们有以下代码须要测试的时候:

export default (fn) => {
  setTimeout(() => {
    fn()
  }, 3000)
}
复制代码

咱们不可能老是去等待定时器,这时候咱们要用 Jest 来操做时间!步骤以下:

  1. 经过 jest.useFakeTimers() 使用 jest “自制的” 定时器,这里放在 beforeEach 里面是由于快进时间可能被调用屡次,我但愿在每一个测试用例里,这个时钟都是初始状态,不会互相影响。
  2. 执行 timer 函数以后,快进时间 3 秒 jest.advanceTimersByTime(3000),这个方法能够调用任意次,快进的时间会叠加。
  3. 这时候咱们已经穿梭到了 3 秒后,expect 也能生效了!

特别说明一下:jest.fn() 生成的是一个函数,这个函数能被监听调用过几回

import timer from './timer/timer'

beforeEach(() => {
  jest.useFakeTimers()
})

it('timer 测试', () => {
  const fn = jest.fn()
  timer(fn)
  jest.advanceTimersByTime(3000)
  expect(fn).toHaveBeenCalledTimes(1)
})
复制代码

3. mock 类

一样的,当咱们只关注类的方法是否被调用,而不关心方法调用产生的结果时,能够 mock 类

在 util/util.js 中定义了 Util 类

export class Util {
  a() {}
  b() {}
}
复制代码

在 util/useUtil 中调用了这个类

import {Util} from './util'

export function useUtil() {
  let u = new Util()
  u.a()
  u.b()
}
复制代码

咱们须要测试 u.a 和 u.b 被调用,jest.mock('./util/util') 会将 Util、Util.a、Util.b 都 mock 成 jest.fn

测试用例以下:

jest.mock('./util/util')  // mock Util 类
import {Util} from './util/util'
import {useUtil} from './util/uesUtil'

test('util 的实例方法被执行了', () => {
  useUtil()
  expect(Util).toHaveBeenCalled()
  expect(Util.mock.instances[0].a).toHaveBeenCalled()
  expect(Util.mock.instances[0].b).toHaveBeenCalled()
})
复制代码

6、结合 Vue组件 进行单元测试

1. 简单用例入门

Vue 提供了 @vue/test-utils 来帮助咱们进行单元测试,建立 Vue 项目的时候勾选测试选项会自动帮咱们安装。

先来介绍两个经常使用的挂载方法:

  • mount:会将组件以及组件包含的子组件都进行挂载
  • shallowMount:浅挂载,只会挂载组件,忽略子组件

再来看一个简单的测试用例:

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.props('msg')).toBe(msg)
  })
})
复制代码

shallowMount 会返回一个 wrapper,这个 wrapper 上面会包含不少帮助咱们测试的方法,详见

2. 快照测试

快照测试的意思是,会将组件像拍照同样拍下来,存底。下次运行测试用例的时候,若是组件发生变化,和快照不同了,就会报错。

测试用例写法以下: 第一次测试会保存 wrapper 的快照,第二次会比较当前 wrapper 和快照的区别

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper).toMatchSnapshot()
  })
})
复制代码

咱们再来看看快照长什么样子:

1580787793(1)
能够看到,快照实际保存的就是组件渲染以后的 html 部分,css 部分没有保存,在元素上绑定的 @click 等一些事件也不会保存, 因此快照适合进行 DOM 节点是否变化的测试。

当快照发生变化时,咱们能够在终端按 u 进行更新快照

1580788050(1)

3. 覆盖率测试

覆盖率测试是对测试彻底程度的一个评估,测试覆盖到的业务代码越多,覆盖率越高。

在 jest.config.js 中咱们能够设置 collectCoverageFrom,来设置须要进行覆盖率测试的文件,这里咱们测试一下全部的 .vue 文件,忽略 node_modules 下全部文件。

要注意,在 Vue 中配置 jest,参考文档

而后添加一条 script 命令,就能进行测试了:

"test:unit": "vue-cli-service test:unit --coverage"
复制代码

执行命令会生成 coverage 文件夹,Icov-report/index.html 里会可视化展现咱们的测试覆盖率

4. 结合 Vuex 进行测试

若是咱们在组件中引入了 Vuex 状态或者使用了相关方法,在测试用例里,挂载组件的时候只须要传入 vuex 的 store 便可。

import store from '@/store/index'

const wrapper = mount(HelloWorld, {
    store
})
复制代码

7、写在最后

1. 单元测试 or 集成测试?

就拿 shallowMount 来讲,这个 api 就很适合单元测试,单元测试不关注单元之间的联系,对每一个单元进行独立测试, 这也使得它代码量大,测试间过于独立。在进行一些函数库的测试,各个函数比较独立的时候,就很适合单元测试。
在进行一些业务组件测试时,须要关注组件间的联系,比较适合用集成测试。

2. TDD or BDD?

TDD:测试驱动开发,先写测试用例,而后根据用例写代码,比较关注代码自己。以下:

describe('input 输入回车,向外触发事件,data 中的 inputValue 被赋值', () => {
  const wrapper = shallowMount(TodoList)
  const inputEle = wrapper.find('input').at(0)
  const inputContent = '用户输入内容'
  inputEle.setValue(inputContent)
  // expect:add 事件被 emit
  except(wrapper.emitted().add).toBeTruthy()
  // expect:data 中的 inputValue 被赋值为 inputContent
  except(wrapper.vm.inputValue).toBe(inputContent)
})
复制代码

TDD 关注代码内部如何实现,关注事件是否触发?属性是否设置?data 数据是否被更新?

BDD:用户行为驱动开发,先写完业务代码,而后站在用户的角度去测试功能,不关注代码实现过程,只是经过模拟用户操做测试功能
好比下面这个用例:

describe('TodoList 测试', () => {
  it(` 1. 用户在 header 输入框输入内容 2. 键盘回车 3. 列表项增长一项,内容为用户输入内容 `, () => {
    // 挂载 TodoList 组件
    const wrapper = mount(TodoList)
    // 模拟用户输入
    const inputEle = wrapper.find('input').at(0)
    const inputContent = '用户输入内容'
    inputEle.setValue(inputContent)
    // 模拟触发的事件
    inputEle.trigger('content')
    inputEle.trigger('keyup.enter')
    // expect:列表项增长对应内容
    const listItems = wrapper.find('.list-item')
    expect(listItems.length).toBe(1)  // 增长 1 项
    expect(listItems.at(0).text()).toContain(inputContent)  // 增长 1 项
  })
})
复制代码

参考:

前端要学的测试课

相关文章
相关标签/搜索