# Jest 介绍
单元测试是函数库中最重要的环节之一,良好的单元测试能对函数库产生许多正面的影响;
- 单元测试能帮助我们节约后续的开发和维护成本,在一定程度上降低了出错的概率;
- 单元测试也是代码设计的一部分,会促使开发者重新审视自己的代码,改进设计,完善各种边界条件和异常情况的处理;
- 当函数被修改后,如果发生错误,单元测试能帮助我们快速定位错误。
常见的单元测试框架有 Mocha (opens new window) 、Jasmine (opens new window)、Jest (opens new window) 等。
Jest 配置简单、功能强大、容易上手,在绝大多数情况下,它都是一个不错的选择。
# 快速开始
安装
npm install --save-dev jest
// 或者
yarn add --dev jest
创建 sum.js
function sum(a, b) {
return a + b
}
module.exports = sum
创建 sum.test.js
const sum = require('./sum')
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})
在 package.json
里面添加如下配置
{
"scripts": {
"test": "jest"
}
}
运行 npm run test
即可看到控制台打印了以下信息
PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.985s, estimated 1s
Ran all test suites.
# 测试单个文件
# 普通
npx jest path
# 生成覆盖率报告
npx jest path --coverage --verbose -u
# 匹配器
Jest 内置了断言,可以使用多种匹配器。
最简单的测试值的方法是看是否精确匹配。
test('two plus two is four', () => {
expect(2 + 2).toBe(4)
})
在此代码中,expect (2 + 2)
返回一个"期望"的对象。 你通常不会对这些期望对象调用过多的匹配器。 在此代码中,.toBe(4)
是匹配器。 当 Jest 运行时,它会跟踪所有失败的匹配器,以便它可以为你打印出很好的错误消息。
toBe
是用来判断是否精确匹配,在判断对象是否相等时,可以使用 toEqual
test('object assignment', () => {
const data = { one: 1 }
data['two'] = 2
expect(data).toEqual({ one: 1, two: 2 })
})
toEqual
会递归检查对象或数组的每个字段。
在测试中,你有时需要区分 undefined、 null,和 false,但有时你又不需要区分。 Jest 能让你明确地表达你想要什么。
test('null', () => {
const n = null
expect(n).toBeNull()
expect(n).toBeDefined()
expect(n).not.toBeUndefined()
expect(n).not.toBeTruthy()
expect(n).toBeFalsy()
})
test('wont', () => {
const z = 0
expect(z).not.toBeNull()
expect(z).toBeDefined()
expect(z).not.toBeUndefined()
expect(z).not.toBeTruthy()
expect(z).toBeFalsy()
})
数字的对比也有相应的匹配器
test('two plus two', () => {
const value = 2 + 2
expect(value).toBeGreaterThan(3)
expect(value).toBeGreaterThanOrEqual(3.5)
expect(value).toBeLessThan(5)
expect(value).toBeLessThanOrEqual(4.5)
// toBe and toEqual are equivalent for numbers
expect(value).toBe(4)
expect(value).toEqual(4)
})
js 浮点数计算中,有可能会出现精度问题,但是没有关系,Jest 提供了相应的解决方案
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2
//expect(value).toBe(0.3) 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3) // 这句可以正常运行
})
我们还可以使用正则表达式来进行匹配
test('there is no I in team', () => {
expect('team').not.toMatch(/I/)
})
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/)
})
以通过 toContain 来检查一个数组或可迭代对象是否包含某个特定项:
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'beer',
]
test('the shopping list has beer on it', () => {
expect(shoppingList).toContain('beer')
expect(new Set(shoppingList)).toContain('beer')
})
当我们要测试的特定函数抛出一个错误,可以在它调用时,使用 toThrow
function compileAndroidCode() {
throw new ConfigError('you are using the wrong JDK')
}
test('compiling android goes as expected', () => {
// expect(compileAndroidCode()).toThrow() 这种写法是错误的,如果想要抛出异常,expect()中必须是一个方法
expect(compileAndroidCode).toThrow()
expect(compileAndroidCode).toThrow(ConfigError)
// You can also use the exact error message or a regexp
expect(compileAndroidCode).toThrow('you are using the wrong JDK')
expect(compileAndroidCode).toThrow(/JDK/)
})
这些只是一部分,有关匹配器的完整列表,请查阅参考文档 (opens new window)。
# 测试异步代码
在 JavaScript 中执行异步代码是很常见的。 当你有以异步方式运行的代码时,Jest 需要知道当前它测试的代码是否已完成,然后它可以转移到另一个测试。 Jest 有若干方法处理这种情况。
回调函数
例如,假设您有一个 fetchData(callback) 函数,获取一些数据并在完成时调用 callback(data)。 你想要测试这返回的数据是只是字符串 'peanut butter'。
test('the data is peanut butter', (done) => {
function callback(data) {
expect(data).toBe('peanut butter')
done()
}
fetchData(callback)
})
必须要在回调方法 callback
中调用 done()
;Jest 回等待 done()
调用后再结束测试;如果 done()
永远不调用,这个测试将失败,这也是你所希望发生的。
Promises
如果您的代码使用 Promises,还有一个更简单的方法来处理异步测试。 只需要从您的测试返回一个 Promise, Jest 会等待这一 Promise 来解决。不要忘记把 Promise 作为返回值:
test('the data is peanut butter', () => {
return fetchData().then((data) => {
expect(data).toBe('peanut butter')
})
})
如果你想要 Promise 被 reject ,可以使用 .catch 方法。 请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则一个 fulfilled 态的 Promise 不会让测试失败︰
test('the fetch fails with an error', () => {
expect.assertions(1)
return fetchData().catch((e) => expect(e).toMatch('error'))
})
resolves、rejects 匹配器
还可以使用 resolves、rejects 匹配器,一定不要忘记把整个断言作为返回值返回:
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter')
})
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error')
})
async 和 await
此外,还可以在测试中使用 async 和 await。 若要编写 async 测试,只要在函数前面使用 async 关键字传递到 test。 例如,可以用来测试相同的 fetchData 方案︰
test('the data is peanut butter', async () => {
const data = await fetchData()
expect(data).toBe('peanut butter')
})
test('the fetch fails with an error', async () => {
expect.assertions(1)
try {
await fetchData()
} catch (e) {
expect(e).toMatch('error')
}
})
# 全局方法
# describe(name, fn)
describe
的回调函数 fn
中可以写多个 test
, name
是一个 string
类型的参数,表示对这多个 test
的描述
describe('minus 方法测试', () => {
test('1 - 2 = -1', () => {
expect(minus(1, 2)).toBe(-1)
})
test('0 - 0 = 0', () => {
expect(minus(0, 0)).toBe(0)
})
test('2 - 1 = 1', () => {
expect(minus(2, 1)).toBe(1)
})
test('1 - undefined to throw Error', () => {
expect(() => {
minus(1)
}).toThrow('minus方法的参数必须为Number类型')
})
})
# beforeEach(fn, timeout)
在每个 test
运行之前,beforeEach
的回调方法 fn
都会运行一次;
let num = 1
beforeEach(() => {
num = 1
})
test('第一个测试', () => {
num += 1
expect(num).toBe(2)
})
test('第二个测试', () => {
num += 2
expect(num).toBe(3)
})
如果 fn
返回一个promise
, Jest 会等待 promise
完成后再运行 test
; 或者,您可以提供timeout
(以毫秒为单位)来指定等待的时间。注意:默认超时为 5000 毫秒.
let num = 1
beforeEach(() => {
return requestFun().then(() => {
num = 1
})
})
test('第一个测试', () => {
num += 1
expect(num).toBe(2)
})
test('第二个测试', () => {
num += 2
expect(num).toBe(3)
})
如果 beforeEach
在 describe 块内,则它将在 describe 块的开头运行。
# beforeAll(fn, timeout)
在 test
运行之前,会运行一次 beforeAll
的回调函数 fn
;
beforeAll
和 beforeEach
的区别是:
beforeAll
: 无论test
有多少个,beforeAll
只会运行一次beforeEach
:test
有多少个,beforeEach
就会运行多少次
其他类似的方法还有 afterEach
、 afterAll
等;
更多用法请参考 Jest 官网 (opens new window)
# Puppeteer
Jest 默认使用 jsdom (opens new window) 来模拟一些浏览器相关的属性,但是和真实表现并不完全一致;
有一些函数会使用 XMLHttpRequest 对象(例如文件上传函数),但是 XMLHttpRequest 是用于浏览器向服务器来发起的,node 环境没有 XMLHttpRequest,虽然有相关的 npm 包来模拟,但是和真正的 XMLHttpRequest 始终有差别,例如文件上传进度的监听等;
这时候,我们可以使用 Puppeteer (opens new window);
Puppeteer 是一个 Chrome 官方出品的 headless Chrome node 库。它提供了一系列的 API, 可以在无 UI 的情况下调用 Chrome 的功能, 适用于爬虫、自动化处理等各种场景。它的功能包括但是不限于以下几点:
- 生成页面截图和 PDF
- 自动化表单提交、UI 测试、键盘输入等
- 创建一个最新的自动化测试环境。使用最新的 JavaScript 和浏览器功能,可以直接在最新版本的 Chrome 中运行测试
- 捕获站点的时间线跟踪,以帮助诊断性能问题
- 抓取 SPA 并生成预先呈现的内容
安装 puppeteer 后会自动下载一个 chromium 内核(100M+),相当于运行在一个真实的浏览器环境中,只是缺少了 UI 页面;也可以自己下载 chromium ,然后指定 chromium 路径;此外,还可以指定本地已安装的 Chrome 浏览器中的 chromium 路径,例如:executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
函数库默认引入了 Puppeteer,下面简单介绍一些如何在 Jest 中使用 Puppeteer。
import puppeteer from 'puppeteer'
test('获取页面title', async () => {
// 创建 browser 实例
const browser = await puppeteer.launch({
args: ['--no-sandbox'],
})
// 创建 page 实例
const page = await browser.newPage()
// 打开页面
await page.goto('https://www.baidu.com')
// 也可以打开本地启动的服务
// await page.goto('http://localhost:8080/')
// 对当前页面截图,命名为 'screenshot.png',并保存到 'src' 目录中
await page.screenshot({ path: 'src/screenshot.png' })
// 关闭浏览器
await browser.close()
})
也可以在 page
中注入本地的 js 文件
// hello.ts
export default function hello(info: string): string {
return `hello ${info}`
}
// hello.test.js
import puppeteer from 'puppeteer'
test('获取页面title', async () => {
jest.setTimeout(20000) // 设置超时时间(毫秒), 默认是5000
const browser = await puppeteer.launch({
args: ['--no-sandbox'],
})
const page = await browser.newPage()
await page.goto('https://www.baidu.html')
// 把 js 文件注入到页面中, path 表示js文件的路径
// 在函数库中,我们把打包好的文件存放在 'lib/index.min.js'
await page.addScriptTag({ path: 'lib/index.min.js' })
const name1 = 'name1'
const name2 = 'name2'
const title = await page.evaluate(
(a, b) => {
document.title = Jax.hello(a + b)
return document.title
},
name1,
name2
)
await browser.close()
expect(title).toBe('hello name1name1')
})
更多姿势请查看 Jest (opens new window) 、Puppeteer (opens new window)