单元测试
单元测试其实在我的实际开发中并没有用到过,但却经常听说,接下来进行单元测试的学习
Jest
和Vue Test Utils
的基础和进阶全覆盖TDD
,测试驱动开发,一种全新的开发方式
测试框架
- 断言
Mock
- 异步支持
- 代码覆盖率
测试框架 jest 简介
特点
- 开箱即用,零配置
- 快
- 内置代码覆盖率
- 容易
mock
安装
代码语言:javascript复制npm i --save-dev jest
查看版本
代码语言:javascript复制npx jest --version
27.5.1
断言示例
代码语言:javascript复制test('test common matcher', () => {
expect(2 2).toBe(4);
});
test('test not equal', () => {
expect(2 2).not.toBe(5);
});
test('test tp be true or false', () => {
expect(1).toBeTruthy();
expect(0).toBeFalsy();
});
test('test number', () => {
expect(4).toBeGreaterThan(3);
expect(2).toBeLessThan(3);
});
test('test object', () => {
expect({ name: 'warbler' }).toEqual({ name: 'warbler' });
});
测试结果
编辑器
如果使用的是 vscode
并且安装了 jest
插件,那么可以实时并且直观的看到测试是否通过
Jest 实现异步测试
回调方式
代码语言:javascript复制// callback
const fetchUser = (cb) => {
setTimeout(() => {
cb("hello")
}, 100)
}
it('test callback', (done) => {
fetchUser((data) => {
expect(data).toBe("hello")
done()
})
})
Promise
需要 return
// promise
const userPromise = () => Promise.resolve("hello")
it('test Promise', () => {
return userPromise().then(data => {
expect(data).toBe("hello")
})
})
async await
代码语言:javascript复制// async await
const userPromise = () => Promise.resolve("hello")
it('test async await', async () => {
const data = await userPromise()
expect(data).toBe("hello")
})
expect
expect
会添加一些属性,也可以获取到 promise
的 reject
和 resolve
,需要 return
。
const userPromise = () => Promise.resolve("hello")
const userPromiseReject = () => Promise.reject("error")
// expect
it('test with expect', () => {
return expect(userPromise()).resolves.toBe("hello")
})
// expect reject
it('test with expect reject', () => {
return expect(userPromiseReject()).rejects.toBe("error")
})
Jest mock
为什么需要 Mock
- 前端需要网络请求
- 后端依赖数据库等模块
- 局限性:依赖其它的模块
Mock 解决方案
- 测试替代,将真实代码替换为替代代码。
Mock 的几大功能
- 创建
mock function
,在测试中使用,用来测试回调 - 手动
mock
,覆盖第三方实现,狸猫换太子 - 三大
API
实现不同粒度的时间控制
函数测试
代码语言:javascript复制function mockTest(shouldCall, cb) {
if (shouldCall) {
return cb(42)
}
}
it('test with mock function', () => {
// 创建一个假的函数实现
const mockCB = jest.fn()
mockTest(true, mockCB)
// 函数是否被调用过了
expect(mockCB).toHaveBeenCalled()
// 是否被参数调用
expect(mockCB).toHaveBeenCalledWith(42)
// 被调用的次数
expect(mockCB).toHaveBeenCalledTimes(1)
// 函数调用
console.log(mockCB.mock.calls);
// 函数调用结果
console.log(mockCB.mock.results);
})
这里结果是 undefined
,因为并没有 mock
函数的实现,所以默认为 undefined
。
function mockTest(shouldCall, cb) {
if (shouldCall) {
return cb(42)
}
}
it('test with mock implementation', () => {
const mockCB = jest.fn(x => x * 2)
mockTest(true, mockCB)
console.log(mockCB.mock.calls);
console.log(mockCB.mock.results);
})
现在 mock
函数的实现, 返回参数的二倍,可以看见 value
变成了 84
function mockTest(shouldCall, cb) {
if (shouldCall) {
return cb(42)
}
}
it('test with mock mockReturnValue', () => {
const mockCB = jest.fn().mockReturnValue(20)
mockTest(true, mockCB)
console.log(mockCB.mock.calls);
console.log(mockCB.mock.results);
})
还可以 mock
函数的返回值,可以看见 value
变成了 20
第三方模块实现
代码语言:javascript复制// 一个真实的网络请求模块
const axios = require('axios')
module.exports = function getUserName(id) {
return axios.get(`http://jsonplaceholder.typicode.com/users/${id}`).then((resp) => {
return resp.data.username
})
}
进行测试
代码语言:javascript复制const getUserName = require('./user')
it('test with mock modules', () => {
return getUserName(1).then((name) => {
console.log(name);
})
})
结果输出了 Bret
接下来使用 jest
进行第三方模块 axios
的 mock
const getUserName = require('./user')
// 先引入 axios 这个模块
const axios = require('axios')
// 调用 jest.mock 接管 axios 模块
jest.mock("axios")
// mock axios.get方法的实现
axios.get.mockImplementation(() => {
return Promise.resolve({ data: { username: 'warbler' } })
})
it('test with mock modules', () => {
return getUserName(1).then((name) => {
console.log(name);
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toHaveBeenCalledTimes(1)
})
})
结果已经变成了 warbler
或者使用 mockReturnValue
直接返回结果,结果是一样的。
axios.get.mockReturnValue(Promise.resolve({ data: { username: 'warbler' } }))
还用更简单的方式,直接返回一个 Promise
的 resolve
axios.get.mockResolvedValue({ data: { username: 'warbler' } })
如果多处对同一个模块进行 mock
,会造成大量重复的工作,可以在根目录下新建 __mocks__
文件夹, 然后新建需要 mock
的模块同名文件 axios.js
,jest
会自动对这个文件夹下的文件进行处理。
const axios = {
get: jest.fn(() => Promise.resolve({ data: { username: "warbler" } }))
}
module.exports = axios
timer mocks
- runAllTimers:执行完所有的
timer
- runOnlyPendingTimers:执行完正在等待的
timer
- advanceTimersByTime:精确控制时间流逝多少
ms
const fetchUser = (cb) => {
setTimeout(() => {
cb("hello")
}, 1000)
}
// 所有的 timer 都被 jest 接管
jest.useFakeTimers();
it('test the callback after 1 sec', () => {
const callback = jest.fn()
fetchUser(callback)
expect(callback).not.toHaveBeenCalled()
// setTimeout 此时是一个 mock function
expect(setTimeout).toHaveBeenCalledTimes(1)
// 一下子执行完所有的 timer
jest.runAllTimers()
// 是否被调用
expect(callback).toHaveBeenCalled()
// 调用的参数
expect(callback).toHaveBeenCalledWith('hello')
})
const loopFetchUser = (cb) => {
setTimeout(() => {
cb('one')
setTimeout(() => {
cb('two')
}, 2000)
}, 1000)
}
it('test the callback in loopFetchUser', () => {
const callback = jest.fn()
loopFetchUser(callback)
// 没有被调用
expect(callback).not.toHaveBeenCalled()
// 执行完正在等待的 timer
jest.runOnlyPendingTimers()
// 调用次数
expect(callback).toHaveBeenCalledTimes(1)
// 上一次调用的参数
expect(callback).toHaveBeenLastCalledWith('one')
// 执行完正在等待的 timer
jest.runOnlyPendingTimers()
// 调用次数
expect(callback).toHaveBeenCalledTimes(2)
// 上一次调用的参数
expect(callback).toHaveBeenLastCalledWith('two')
})
it('test the callback in advance timer', () => {
const callback = jest.fn()
loopFetchUser(callback)
// 没有被调用
expect(callback).not.toHaveBeenCalled()
// 控制时间流逝多少ms
jest.advanceTimersByTime(500)
// 控制时间流逝多少ms
jest.advanceTimersByTime(500)
// 调用次数
expect(callback).toHaveBeenCalledTimes(1)
// 控制时间流逝多少ms
expect(callback).toHaveBeenLastCalledWith('one')
jest.advanceTimersByTime(2000)
// 调用次数
expect(callback).toHaveBeenCalledTimes(2)
// 上一次调用的参数
expect(callback).toHaveBeenLastCalledWith('two')
})