Jest难点进阶

snapshot 快照测试

快照的使用
const generateConfig= () => ({
  host: 'localhost',
  port: 3000
})

test('test config snapshot', () => {
  expect(generateConfig()).toMatchSnapshot()
})

test('test another config snapshot', () => {
  expect(generateAnotherConfig()).toMatchSnapshot()
})

初次执行会生成快照文件,第二次执行同样生成快照文件,并与第一次快照进行比较,快照内容相同则测试通过。

快照比对失败,可以用 u 键批量更新快照,也可以用 i 键来决定每一个快照的更新操作。

如果 generateConfig 返回的配置信息在每次函数执行都会发生变化,难道需要我们每次都去更新快照,那么这种情况该如何处理呢?

比如

const generateConfig= () => ({
  host: 'localhost',
  port: 3000,
  time: new Date()
})

这里的 time 字段在每次函数执行的时候都会更新,从而导致快照比对失败,那么能否在进行快照对比的时候自动忽略掉 time 字段的比对呢,其实是可以的

test('test config snapshot', () => {
  expect(generateConfig()).toMatchSnapshot({
    time: expect.any(Date)
  })
})
内联快照

内联快照-不会生成新的快照文件,而是将快照内容写入当前执行测试的文件中

其中在生成内联快照的时候需要第三方的包 prettier 配合

test('test config snapshot', () => {
  expect(generateConfig()).toMatchInlineSnapshot({
    time: expect.any(Date),
    // 内联快照会放在这里
  })
})

总结:快照测试常用于配置文件的测试、React、Vue组件等的测试

mock深入学习

mock 接口请求

Jest基础入门 中实现过接口请求的模拟,如下:

import axios from 'axios'
import { getData } from './api'

jest.mock('axios')

test.only('test axios getData', async () => {
  // mock 改变函数的内部实现
  axios.get.mockResolvedValue({ data: 'hello' })
  await getData().then((response) => {
    expect(response).toBe('hello')
  })
})

这种方法是通过改变 axios 内部方法实现来进行的模拟,这样所有的 get 请求都会被拦截到

另一种方式是对 getData 方法的 mock

jest.mock('./api')
import { getData } from './api'

test.only('test axios getData', async () => {
  await getData().then((response) => {
    expect(response).toBe('hello')
  })
})

通过mock('./api') 这个文件,在下文导入 getData 这个方法的时候,不再从 './api' 文件中导入,而是去 __mocks__ 文件夹下对应的 api 文件中查找并导入,通过这种方式实现对 getData 方法的拦截处理

取消mock
jest.unmock('./api')

在实际的开发中,无需手动进行 unmock 操作,只需在 jest.config.js 中打开 clearMocks 选项即可,该选项会自动清理 mock 调用

局部mock

什么是局部mock,即只针对一个文件中的部分内容进行mock,另一部分保留原始内容,不进行模拟

比如:

// util.js
import axios from 'axios'

export const getConfig = () => {
  return axios.get('/config')
}

export const getNumber => () => 12345

在编写测试用例的时候,只想对 getConfig 方法进行mock,getNumber 使用真实值,不进行mock

这时就用上了 jest.requireActual 方法

jest.mock('./util')
import { getConfig } from './util'
const { getNumber } = jest.requireActual('./util')

test('test axios getConfig', async () => {
  // 从 __mocks__下 util 文件中查找
  await getConfig().then((response) => {
    expect(response).toMatchObject({
      success: true
    })
  })
})

test('test getNumber', () => {
  // 使用真实函数
  expect(getNumber()).toEqual(12345)
})

mock timers

对定时器进行测试

const timerFunc = (callback) => {
	setTimeout(() => {
    callback()
  }, 3000)
}

test('test timer', (done) => {
  timerFunc(() => {
    expect(1).toBe(1)
    done()
  })
})

如果定时器时间过长,则测试时间也会很长,那么如何mock timer呢

// mock timer
jest.useFakeTimers()

test('test timer', () => {
  timerFunc(() => {
    const fn = jest.fn()
    timer(fn)
    // 立即执行全部定时器
    jest.runAllTimers()
    // jest.runOnlyPendingTimers() 只运行处于已加入队列中的定时器
		expect(fn).toHaveBeenCalledTimes(1)
  })
})

当不知道该使用 runAllTimers 还是 runOnlyPendingTimers时,可以使用一个API进行替代,就是 advanceTimersByTime,其可以快进指定的时间,来让定时器提前执行,比如

// 定时器快进 3s 执行
jest.advanceTimersByTime(3000)

为了保证每一个测试用例 timer 相互隔离,可以将 useFakeTimers 提至 beforeEach 钩子函数中

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

ES6中类的测试

class 中负责逻辑的测试

// util.js
class Util {
  init() {
    
  }
  complex() {
    
  }
}

export default Util

实例化 Util

let util
beforeAll(() => {
	util = new Util()
})

实际开发中一般对 Util 进行 mock

// jest mock 发现util是一个类,会自动把类的构造函数和方法变成 jest.fn()
jest.mock('./util')
import Util from './util'

let util
beforeAll(() => {
	util = new Util()
})

test('test Util', () => {
  expect(Util).toHaveBeenCalled()
  util.complex('complex')
  expect(Util.mock.instances[0].complex).toHaveBeenCalled()
})

也可以采用自定义mock,在 __mocks__ 下创建 util.js

const Util = jest.fn(() => {
  console.log('constructor')
})

Util.prototype.complex = ject.fn(() => {
  console.log('complex')
})

export default Util

自定义mock的另一种方式

jest.mock('./util', () => {
  const Util = jest.fn(() => {
    console.log('constructor')
  })

  Util.prototype.complex = ject.fn(() => {
    console.log('complex')
  })
  
  return Util
})

这里推荐在 __mocks__ 文件夹下进行自定义mock

jest中对DOM节点操作的测试

创建DOM节点

import $ from 'jquery'

const addDivToBody = () => {
  $('body').append('<div/>')
}

test('test dom', () => {
  addDivToBody()
  expect($('body').find('div').length).toBe(1)
})

Nodejs不具备dom,jest在node环境中自己模拟了一套dom环境 jsDom