测试框架 Jest 实例教程

Jest 是由 Facebook 开源出来的一个测试框架,它集成了断言库、mock、快照测试、覆盖率报告等功能。它很是适合用来测试 React 代码,但不只仅如此,全部的 js 代码均可以使用 Jest 进行测试。node

本文全面的介绍如何使用 Jest,让后来者轻松上手。文中会选取重点部分直接贴出代码,比较简单的部分则不会,主要是写到后面的时候发现贴的代码有点多,没什么意思,全部的代码已上传到 Github,能够自行查阅。react

安装

使用 yarn 安装 Jest:webpack

$ yarn add --dev jest
复制代码

或使用 npmgit

$ npm i -D jest
复制代码

其中 --dev-D 参数指明做为 devDependencies,这样该依赖只会在开发环境下安装,在生成环境下则不会。github

package.json 文件中添加下面的内容:web

"scripts": {
  "test": "jest"
}
复制代码

这样咱们就能够经过 yarn testnpm test 执行测试代码。数据库

一样地,你也能够选择全局安装 Jest:npm

$ yarn global add jest
$ # or npm i -g jest
复制代码

这样你就能够直接在命令行使用 jest 命令。若是你是本地安装,可是也想在命令行使用 jest,能够经过 node_modules/.bin/webpack 访问它的 bin 版本,若是你的 npm 版本在 5.2.0 以上,你也能够经过 npx jest 访问。json

使用 Babel

若是你在代码中使用了新的语法特性,而当前 Node 版本不支持,则须要使用 Babel 进行转义。数组

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

注意:若是你使用 babel 7,安装 babel-jest 的同时还须要安装其余依赖: npm i -D babel-jest 'babel-core@^7.0.0-0' @babel/core

Jest 默认使用 babel-jest(须要安装) 进行代码转义,若是你须要添加额外的预处理器,则须要在 Jest 配置文件中显示的定义 babel-jest 做为 JavaScript 处理器(由于一旦添加了 transform 配置,babel-jest 就不会自动载入了):

"transform": {
  "^.+\\.jsx?$": "babel-jest"
},
复制代码

咱们还需在根目录下建立 .babelrc 文件:

{
  "presets": [
    "env"
  ]
}
复制代码

我这里只使用了 babel-preset-env 预设,若是须要其余的转换,见 babel

基本用法

咱们从一个基本的 Math 模块开始。首先建立一个 math.js 文件:

// basic/math.js

const sum = (a, b) => a + b
const mul = (a, b) => a * b
const sub = (a, b) => a - b
const div = (a, b) => a / b

export { sum, mul, sub, div }
复制代码

要测试这个 Math 模块是否正确,咱们须要编写测试代码。一般,测试文件与所要测试的源码文件同名,可是后缀名为 .test.js 或者 .spec.js。咱们这里则建立一个 math.test.js 文件:

// basic/math.test.js

import { sum, mul, sub, div } from './math'

test('Adding 1 + 1 equals 2', () => {
  expect(sum(1, 1)).toBe(2)
})

test('Multiplying 1 * 1 equals 1', () => {
  expect(mul(1, 1)).toBe(1)
})

test('Subtracting 1 - 1 equals 0', () => {
  expect(sub(1, 1)).toBe(0)
})

test('Dividing 1 / 1 equals 1', () => {
  expect(div(1, 1)).toBe(1)
})
复制代码

执行 npm test Jest 将会执行全部匹配的测试文件,并最终返回测试结果:

在编辑器中运行

不少编辑器都能支持 Jest,如:Webstorm、VS Code、Atom 等。这里简单地介绍下如何在 Webstorm 和 VS Code 中运行。

Webstorm

Webstorm 可能出现找不到变量等问题,在 Preferences | Languages & Frameworks | JavaScript | Libraries 中点击 Download, 而后选择 Jest 并下载便可。

Webstorm 能够识别测试代码,在编辑器中点击“相应的运行按钮”便可运行,或使用快捷键 ctrl+shift+R(mac 中)。具体的操做能够参考我以前写的 Node.js 中 使用 Mocha 进行单元测试的博客。

VS Code

要想在 VS Code 中运行,咱们须要安装 Jest 插件

插件安装完成后,若是你安装了 Jest,它会自动的运行测试代码。你能够能够手动的运行经过 Jest: Start Runner 命令,它会执行测试代码并在文件发生修改后从新运行。

匹配器

匹配器用来实现断言功能。在前面的例子中,咱们只使用了 toBe() 匹配器:

test('Adding 1 + 1 equals 2', () => {
  expect(sum(1, 1)).toBe(2)
})
复制代码

在此代码中,expect(sum(1, 1)) 返回一个“指望”对象,.toBe(2) 是匹配器。匹配器将 expect() 的结果(实际值)与本身的参数(指望值)进行比较。当 Jest 运行时,它会跟踪全部失败的匹配器,并打印出错误信息。

经常使用的匹配器以下:

  • toBe 使用 Object.is 判断是否严格相等。
  • toEqual 递归检查对象或数组的每一个字段。
  • toBeNull 只匹配 null
  • toBeUndefined 只匹配 undefined
  • toBeDefined 只匹配非 undefined
  • toBeTruthy 只匹配真。
  • toBeFalsy 只匹配假。
  • toBeGreaterThan 实际值大于指望。
  • toBeGreaterThanOrEqual 实际值大于或等于指望值
  • toBeLessThan 实际值小于指望值。
  • toBeLessThanOrEqual 实际值小于或等于指望值。
  • toBeCloseTo 比较浮点数的值,避免偏差。
  • toMatch 正则匹配。
  • toContain 判断数组中是否包含指定项。
  • .toHaveProperty(keyPath, value) 判断对象中是否包含指定属性。
  • toThrow 判断是否抛出指定的异常。
  • toBeInstanceOf 判断对象是不是某个类的实例,底层使用 instanceof

全部的匹配器均可以使用 .not 取反:

test('Adding 1 + 1 does not equal 3', () => {
  expect(1 + 1).not.toBe(3)
})
复制代码

对于 Promise 对象,咱们可使用 .resolves.rejects

// .resolves
test('resolves to lemon', () => {
  // make sure to add a return statement
  return expect(Promise.resolve('lemon')).resolves.toBe('lemon')
})

// .rejects
test('rejects to octopus', () => {
  // make sure to add a return statement
  return expect(Promise.reject(new Error('octopus'))).rejects.toThrow(
    'octopus',
  )
})
复制代码

异步测试

JavaScript 代码中经常会包含异步代码,当测试异步代码时,Jest 须要知道何时异步代码执行完成,在异步代码执行完以前,它会去执行其余的测试代码。Jest 提供了多种方式测试异步代码。

回调函数

当执行到测试代码的尾部时,Jest 即认为测试完成。所以,若是存在异步代码,Jest 不会等待回调函数执行。要解决这个问题,在测试函数中咱们接受一个参数叫作 done,Jest 将会一直等待,直到咱们调用 done()。若是一直不调用 done(),则此测试不经过。

// async/fetch.js
export const fetchApple = (callback) => {
  setTimeout(() => callback('apple'), 300)
}

// async/fetch.test.js
import { fetchApple } from './fetch'

test('the data is apple', (done) => {
  expect.assertions(1)
  const callback = data => {
    expect(data).toBe('apple')
    done()
  }

  fetchApple(callback)
})
复制代码

expect.assertions(1) 验证当前测试中有 1 处断言会被执行,在测试异步代码时,能确保回调中的断言被执行。

Promise

若是异步代码返回 Promise 对象,那咱们在测试代码直接返回该 Promise 便可,Jest 会等待其 resolved,若是 rejected 则测试不经过。

test('the data is banana', () => {
  expect.assertions(1)
  return fetchBanana().then(data => expect(data).toBe('banana'))
})
复制代码

若是指望 promise 是 rejected 状态,可使用 .catch()

test('the fetch fails with an error', () => {
  expect.assertions(1)
  return fetchError().catch(e => expect(e).toMatch('error'))
})
复制代码

除此以外,还可使用上文中提到的 .resolves.rejects

Async/Await

若是异步代码返回 promise,咱们还可使用 async/await:

test('async: the data is banana', async () => {
  expect.assertions(1)
  const data = await fetchBanana()
  expect(data).toBe('banana')
})

test('async: the fetch fails with an error', async () => {
  expect.assertions(1)
  try {
    await fetchError()
  } catch (e) {
    expect(e).toMatch('error')
  }
})
复制代码

也能够将 aysnc/awiat 与 .resolves.rejects 结合:

test('combine async with `.resolves`', async () => {
  expect.assertions(1)
  await expect(fetchBanana()).resolves.toBe('banana')
})
复制代码

钩子函数

Jest 为咱们提供了四个测试用例的钩子:beforeAll()afterAll()beforeEach()afterEach()

beforeAll()afterAll() 会在全部测试用例以前和全部测试用例以后执行一次beforeEach()afterEach() 会在每一个测试用例以前和以后执行。

分组

咱们可使用 describe 将测试用例分组,在 describe 块中的钩子函数只做用于块内的测试用例:

beforeAll(() => console.log('1 - beforeAll')) // 1
afterAll(() => console.log('1 - afterAll')) // 12
beforeEach(() => console.log('1 - beforeEach')) // 2,6
afterEach(() => console.log('1 - afterEach')) // 4,10
test('', () => console.log('1 - test')) // 3
describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll')) // 5
  afterAll(() => console.log('2 - afterAll')) // 11
  beforeEach(() => console.log('2 - beforeEach')) // 7
  afterEach(() => console.log('2 - afterEach')) // 9
  test('', () => console.log('2 - test')) // 8
})
复制代码

须要注意的是,顶级的 beforeEach 会在 describe 块内的 beforeEach 以前执行。

Jest 会先执行 describe 块内的操做,等 describe 块内的操做执行完毕后,按照出如今 describe 中的前后顺序执行测试用例,所以初始化和销毁操做应该放在钩子函数中运行,而不是 describe 块内:

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

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

  console.log('describe outer-b') // 3

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

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

  console.log('describe outer-c') // 5
})
复制代码

Mocks

在测试中,mock 可让你更方便的去测试依赖于数据库、网络请求、文件等外部系统的函数。 Jest 内置了 mock 机制,提供了多种 mock 方式已应对各类需求。

Mock 函数

函数的 mock 很是简单,调用 jest.fn() 便可得到一个 mock 函数。 Mock 函数有一个特殊的 .mock 属性,保存着函数的调用信息。.mock 属性还会追踪每次调用时的 this

// mocks/forEach.js
export default (items, callback) => {
  for (let index = 0; index < items.length; index++) {
    callback(items[index])
  }
}

import forEach from './forEach'

it('test forEach function', () => {
  const mockCallback = jest.fn(x => 42 + x)
  forEach([0, 1], mockCallback)

// The mock function is called twice
  expect(mockCallback.mock.calls.length).toBe(2)

// The first argument of the first call to the function was 0
  expect(mockCallback.mock.calls[0][0]).toBe(0)

// The first argument of the second call to the function was 1
  expect(mockCallback.mock.calls[1][0]).toBe(1)

// The return value of the first call to the function was 42
  expect(mockCallback.mock.results[0].value).toBe(42)
})
复制代码

除了 .mock 以外,Jest 还未咱们提供了一些匹配器用来断言函数的执行,它们自己只是检查 .mock 属性的语法糖:

// The mock function was called at least once
expect(mockFunc).toBeCalled();
复制代码

使用 mockReturnValuemockReturnValueOnce 能够 mock 函数的返回值。 当咱们须要为 mock 函数增长一些逻辑时,可使用 jest.fn()mockImplementation 或者 mockImplementationOnce mock 函数的实现。 还可使用 mockName 还给 mock 函数命名,若是没有命名,输出的日志默认就会打印 jest.fn()

Mock 定时器

Jest 能够 Mock 定时器以使咱们在测试代码中控制“时间”。调用 jest.useFakeTimers() 函数能够伪造定时器函数,定时器中的回调函数不会被执行,使用 setTimeout.mock 等能够断言定时器执行状况。当在测试中有多个定时器时,执行 jest.useFakeTimers() 能够重置内部的计数器。

执行 jest.runAllTimers(); 能够“快进”直到全部的定时器被执行;执行 jest.runOnlyPendingTimers() 可使当前正在等待的定时器被执行,用来处理定时器中设置定时器的场景,若是使用 runAllTimers 会致使死循环;执行 jest.advanceTimersByTime(msToRun:number),能够“快进”执行的毫秒数。

Mock 模块

模块的 mock 主要有两种方式:

  • 使用 jest.mock(moduleName, factory, options) 自动 mock 模块,jest 会自动帮咱们 mock 指定模块中的函数。其中,factoryoptions 参数是可选的。factory 是一个模块工厂函数,能够代替 Jest 的自动 mock 功能;options 用来建立一个不存在的须要模块。
  • 若是但愿本身 mock 模块内部函数,能够在模块平级的目录下建立 __mocks__ 目录,而后建立相应模块的 mock 文件。对于用户模块和 Node 核心模块(如:fs、path),咱们仍须要在测试文件中显示的调用 jest.mock(),而其余的 Node 模块则不须要。

此外,在 mock 模块时,jest.mock() 会被自动提高到模块导入前调用。

对于类的 mock 基本和模块 mock 相同,支持自动 mock、手动 mock 以及调用带模块工厂参数的 jest.mock(),还能够调用 jest.mockImplementation() mock 构造函数。

快照测试

快照测试是 Jest 提供的一个至关棒的 UI 测试功能,它会记录 React 结构树快照或其余可序列化的值,并与当前测试的值进行比较,若是不匹配则给出错误提示。快照应该被当作代码来对待,它须要被提交到版本库并进行 Review。

若是组件渲染结果发生变化,测试将会失败。当组件正常调整时,咱们能够调用 jest -u 更新快照。在监控模式下,咱们能够经过交互式的命令更新快照。

下面经过一个简单的 text 组件来测试一下:

// Text.js

import React from 'react'

export default ({className, children}) => {
  return (
    <span className={className}>{children}</span>
  )
}
复制代码

除了 react 咱们还须要安装依赖:npm i -D babel-preset-react react-test-renderer,其中 babel-preset-react 预设用来解析 jsx 语法,须要添加到 babel 配置中。

测试代码以下:

// Text.test.js

import React from 'react'
import renderer from 'react-test-renderer'

import Text from './Text'

it('render correctly', () => {
  const tree = renderer
    .create(<Text className="success">Snapshot testing</Text>)
    .toJSON()
  expect(tree).toMatchSnapshot()
})
复制代码

执行测试代码后,会生成以下快照:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`render correctly 1`] = `
<span
  className="success"
>
  Snapshot testing
</span>
`;
复制代码

若是后续修改致使组件渲染结果发生变化,快照将会不匹配,测试则不经过。

Jest 命令行

jest 命令行工具备有用的选项。运行 jest -h 能够查看全部可用的选项。全部的 Jest 的 配置项均可以经过命令行来指定。

基本用法:jest [--config=<pathToConfigFile>] [TestPathPattern] 生成配置信息:jest --init 运行符合指定用模板或文件名的测试︰jest path/to/my-test.js 启动监视模式︰jest --watch 生成覆盖率报告:jest --coverage

Jest 配置

Jest 的一个理念是提供一套完整集成的“零配置”测试体验,开发人员能够直接上手编写测试用例。它为咱们集成了测试经常使用的工具,多数状况下使用默认配置或少许的调整便可。

Jest 的配置能够定义在 package.jsonjest.config.js 文件中或经过命令行参数 --config <path/to/js|json>。配置并非必须的,具体内容见文档,按需取用便可。

PS:Jest 中 testURL 的默认值是 about:blank,在 jsdom 环境下运行会报错,设置了 testURL 为一个有效的 URL 后可以避免这个问题,如:http://localhost

相关文章
相关标签/搜索