初识Jest

852 阅读9分钟

一、起步

Jest 是什么?

Jest 是用来创建、执行和构建测试用例的一个 JavaScript 测试库, 是目前最受欢迎的测试执行器。

首先,使用 yarn 或 npm 安装 Jest︰

yarn add --dev jest or npm install --save-dev jest

接着,创建一个 sum.js 的文件:

export default function sum(a, b) {  
    return a + b
}

然后,创建一个名为 sum.test.js 的文件。 这将包含我们的实际测试:

import sum from './sum'

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3)
})

将下面的配置部分添加到你的 package.json 里面:

{
  "scripts": {
    "test": "jest"
  }
}
最后,运行 yarn test 或 npm run test,Jest将打印以下这个消息:
PASS  ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

如果想要支持TypeScript ,可以使用 ts-jest.

二、匹配器

测试值最简单的方法是看是否精确匹配期望。

test('2 + 2 = 4', () => {
  expect(2 + 2).toBe(4)
})

在上面的代码中,expect (2 + 2) 返回一个"期望"的对象,.toBe(4) 是匹配器。 当 Jest 运行时,它会跟踪所有失败的匹配器,以便它可以为你打印出很好的错误消息。

toBe 使用的是 Object.is 来测试精确相等。 如果想要检查对象的值,使用 toEqual 代替:

test('object assignment', () => {
  const data = {name: 'Jest', age:12}
  expect(data).toEqual({name: 'Jest', age:12});});

toEqual 会递归检查对象或数组的每个字段。还可以使用 not 测试相反的匹配︰

test('1 is not 0', () => {  
    expect(1).not.toBe(0)
})

其他的常用匹配器:

  • toBeNull 只匹配 null
  • toBeUndefined 只匹配 undefined
  • toBeDefined 与 toBeUndefined 相反
  • toBeTruthy 匹配任何 if 语句为真
  • toBeFalsy 匹配任何 if 语句为假
  • toBeGreaterThan 大于
  • toBeGreaterThanOrEqual 大于等于
  • toBeLessThan 小于
  • toBeLessThanOrEqual 小于等于
  • toMatch 正则匹配
  • toContain 包含
  • toThrow 抛出错误

三、测试异步代码

1. 回调

假设有一个 fetchData(callback) 函数,获取一些数据并在完成时调用 callback(data)。 期望返回的数据是一个字符串 'callback done'

默认情况下,Jest 测试一旦执行到末尾就会完成。 这意味着该测试不会按预期工作:

// 错误做法
test('the data is callback done', () => {
  function callback(data) {
    expect(data).toBe('callback done')
  }

  fetchData(callback)
})

fetchData执行结束,此测试就在没有调用回调函数前结束了。

解决方法:使用单个参数调用 done,而不是将测试放在一个空参数的函数。 Jest会等done回调函数执行结束后,结束测试。

test('the data is callback done', done => {
  function callback(data) {
    try {
      expect(data).toBe('callback done')
      done()
    } catch (error) {
      done(error)
    }
  }

  fetchData(callback)
})

done() 函数从未被调用,测试用例会正如你预期的那样执行失败(显示超时错误)。

expect 执行失败,它会抛出一个错误,后面的 done() 不再执行。 若我们想知道测试用例为何失败,我们必须将 expect 放入 try 中,将 error 传递给 catch 中的 done函数。 否则,最后控制台将显示一个超时错误失败,不能显示我们在 expect(data) 中接收的值。

2.Promise

如果fetchData返回一个Promise,可以这样测试:

test('the data is success', () => { 
    return fetchData().then(data => { 
        expect(data).toBe('success')
    }) 
})

注意:一定不要忘记把 promise 作为返回值。
如果忘了 return 语句的话,在 fetchData 返回的这个 promise 被 resolve、then() 有机会执行之前,测试就已经被视为已经完成了。

如果期望promise被reject的话,使用.catch方法。请确保添加 expect.assertions 来验证一定数量的断言被调用。否则,一个已兑现的承诺就不会失败。

test('the fetch fails with an error', () => {
  expect.assertions(1)
  return fetchData().catch(e => expect(e).toMatch('error'))
})

除了上面的方法,还可以使用.resolves / .rejects 匹配器:

test('the data is success', () => {
  return expect(fetchData()).resolves.toBe('success');
})

3. Async/Await

test('the data is success', async () => {
  const data = await fetchData()
  expect(data).toBe('success')
});

test('the fetch fails with an error', async () => {
  expect.assertions(1)
  try {
    await fetchData()
  } catch (e) {
    expect(e).toMatch('error')
  }
})

四、预处理和后处理

有时我们想在测试开始之前进行下环境的检查、或者在测试结束之后作一些清理操作,这就需要对用例进行预处理或后处理。

对测试文件中所有的用例进行统一的预处理,使用 beforeAll() 函数;如果想在每个用例开始前进行都预处理,则可使用 beforeEach() 函数。至于后处理,也有对应的 afterAll()afterEach() 函数。

如果只是想对某几个用例进行同样的预处理或后处理,可以使用 describe() 函数表示一组用例,再将上面提到的四个处理函数置于 describe() 的处理回调内,就实现了对一组用例的预处理或后处理。

describe 和 test 块的执行顺序

Jest 会在测试开始之前执行测试文件里所有的 describe 处理程序(handlers)。 当 describe 块运行完后,,默认情况下,Jest 会按照 test 出现的顺序依次运行所有测试,,等待每一个测试完成并整理好,然后才继续往下走。

因此,准备工作和整理工作最好在 before*after* 处理程序里面 (而不是在 describe 块中)进行。

describe('outer', () => {
  console.log('describe outer-a')

  test('test for describe inner 1', () => {
    console.log('test for describe inner 1')
    expect(true).toEqual(true)
  })

  describe('describe inner', () => {
    console.log('describe inner')
    test('test for describe inner 2', () => {
      console.log('test for describe inner 2')
      expect(false).toEqual(false)
    })
  })

  console.log('describe outer-b')
})

// describe outer-a
// describe inner
// describe outer-b
// test for describe inner 1
// test for describe inner 2

五、Mock 函数

Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 new 实例化时捕获构造函数的实例、允许测试时配置返回值。

假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数。

function forEach(arr, callback) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i])
  }
}

为了测试此函数,我们可以使用一个 mock 函数,然后通过检查 mock 函数的状态来确保callback如期调用。

const mockCallback = jest.fn(x => 8 + x)
forEach([0, 1], mockCallback)

// 此 mock 函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2)

// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0)

// 第二次调用函数时的第一个参数是 1
expect(mockCallback.mock.calls[1][0]).toBe(1)

// 第一次函数调用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(8)

所有的 mock 函数都有一个特殊的 .mock属性,它保存了关于此函数如何被调用、调用时的返回值的信息。同时jest也为我们提供了toHaveBeenCalledtoHaveBeenCalledTimestoHaveBeenCalledWith等自定义的匹配器。

注意:在Jest中如果想捕获函数的调用情况,则该函数必须被mock或者spy!

Mock 的返回值

const mockFn = jest.fn()
console.log(mockFn())// undefined

mockFn.mockReturnValueOnce(10).mockReturnValue(true)

console.log(mockFn(), mockFn(), mockFn())// 10, true, true

模拟模块

有时需要测试该方法而不实际调用 API ,比如在使用请求时会导致测试缓慢而且脆弱,这时可以用 jest.mock() 函数自动模拟 axios 模块。

模拟axios模块,我们可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。

import axios from 'axios'
jest.mock('axios')
test('should fetch users', () => {  
    const users = [{ name: 'Jest' }] 
    const response = { data: users }  
    axios.get.mockResolvedValue(response)  
    return axios    
    .get('/users.json')    
    .then(resp => resp.data)    
    .then(data => expect(data).toEqual(users))
})

六、Timer Mock

原生的定时器函数(如:setTimeout, setInterval, clearTimeout, clearInterval)并不是很方便测试,因为程序需要等待相应的延时。

我们可以通过jest.useFakeTimers()来模拟定时器函数。如果需要在一个文件或一个describe块中运行多次测试,可以在每次测试前手动添加jest.useFakeTimers(),或者在beforeEach中添加。 如果不这样做的话内部的定时器不会被重置。

  • runAllTimers 快进时间使得所有定时器回调被执行
  • runOnlyPendingTimers 只快进进行中的定时器
  • advanceTimersByTime 按时间提前定时器
  • clearAllTimers 删除所有待处理的计时器

七、Jest 覆盖率

在运行时我们可以直接增加 --coverage参数,生成coverage报表查看当前项目的覆盖率

"scripts": {
    "test": "jest --coverage"
}

现在,每次运行 npm run test 时,我们项目下就会产生coverage报表来查看当前项目的覆盖率

八、遇到的一些问题及解决办法

通用建议

如果测试失败,第一件要检查的事就是,当仅运行这条测试时,它是否仍然失败。

test.only('this will be the only test that runs', () => {
  expect(true).toBe(false)
});

test('this test will not run', () => {
  expect('A').toBe('A')
})

如果你有一个测试作为一个更大的用例中的一部分时,运行失败。但是当你单独运行它时,并不会失败,所以最好考虑其他测试对这个测试的影响。 通常可以通过修改 beforeEach 来清除一些共享的状态来修复这种问题。

Jest 不会执行资源的onload事件

解决办法:

方案1——通过手动触发执行

beforeAll(() => {  
    Object.defineProperty(global.Image.prototype, 'src', {    
        set(src) {      
            if (src === 'LOAD_FAILURE_SRC') {        
                setTimeout(() => this.onerror(new Error('mocked error')))      
            } else if (src === 'LOAD_SUCCESS_SRC') {        
                setTimeout(() => this.onload())          
            }    
        }  
    })
})

方案2.——安装 canvas

yarn add canvas -D

package.json 中增加如下配置:

"testEnvironmentOptions":  {  "resources": "usable"}

原因:

当浏览器配置为不加载图像时,不会触发加载事件。jsdom默认配置为不加载任何外部资源,因此在这方面它遵循浏览器。这会使图像与其他未加载的资源(如视频、音频、linkrel=preload等)保持一致

所以我们可以切换到加载“usable”外部资源的模式,但是只有在安装了canvas包时,才包括图像。

所以只有在安装了canvas包时才会执行该测试,在这种情况下,加载事件会触发。

Jest 中某些API不存在

原因:

Jest的实现依赖于JsDom,如果存在JsDom中没有实现的API,则无法使用

解决方案: 手动mock

如果我们的测试中使用了window.matchMedia()。Jest将会返回 TypeError: window.matchMedia is not a function 导致测试出错。

Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(), // deprecated
    removeListener: jest.fn(), // deprecated
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn()
  }))
})

Jest测试随机性函数

mock 随机函数,返回固定值

测试时间:

const mockDate = new Date(1466424490000)
jest.spyOn(global, 'Date').mockImplementation(() => mockDate)

测试随机数:

global.Math.random = jest.fn(() => 0.5)

扩展