测试的重要性
1 )出现的问题
- 新建一堆不同属性的组件,肉眼观测
- 更新代码,要再次进行手动测试
2 )测试在国内被严重忽视
- 没有时间
- 需求一直修改
- 不知道怎样写测试
3 )测试的优点
- 自动化完成流程,保证代码的运行结果
- 更早发现 Bug
- 重构和升级更加容易和可靠
4 )测试金字塔
4.1 Martin Fowler 在2012年提出
![](https://i-blog.csdnimg.cn/direct/69675f89047645f697ecc4cc038e8ee0.png)
- 底座:Unit Test, 单元测试,没有相互依赖的,独立的各个功能
- 中间:Service Test, 用于组合Unit Test
- 顶层:UI Test 或者 E2E Test,模拟真实用户场景,填写表单,模拟点击等操作
- 以上看出,单元测试 占比非常多,非常容易编写和修改维护,效率高
4.2 国内测试情况,倒金字塔型
![](https://i-blog.csdnimg.cn/direct/fb47e9da613b48c187177595d2129b28.png)
- 顶部是手动测试,占比非常大
- 之后是UI测试,QA写一系列测试驱动程序,模拟用户操作来测试
- 之后是服务测试,和单元测试,属于开发人员范畴,几乎不写
鉴于国内情况特殊,我们并不推荐,对于一个完善的组件库产品,我们倾向于对其进行编写单元测试
组件化的框架(React,Vue)非常适合写单元测试
- 组件化,独立单元,互不影响
- 属性和界面的映射,固定输入,固定输出
- 单向数据流,只需要测试是否触发相应的回调和发送对应事件
- 状态管理工具的 store 可以单独写测试,数据的变化都在store中
通用测试框架
测试框架的几大功能
- 断言
- Jest 内置
- Mocha 需要另外安装 Chai 或者其他断言库
- 异步支持: Jest 和 Mocha 支持良好
- Mock
- Jest 内置
- Mocha 需要另外安装 Sinon
- 代码覆盖率
- Jest 内置
- Mocha 需要另外安装 Istanbul
1 )Jest的特点
- 开箱即用,零配置
- 快!
- 内置代码覆盖率
- Mocking 很容易
2 )Mocha的特点
- 灵活
- 可扩展
Jest
- 查看 Jest 版本: $
npx jest --version
- 测试文件命名
xx.test.js
- 断言:using-matchers
- toBeNull matches only null
- toBeUndefined matches only undefined
- toBeDefined is the opposite of toBeUndefined
- toBeTruthy matches anything that an if statement treats as true
- toBeFalsy matches anything that an if statement treats as false
- 执行测试文件
- $
npx jest
执行一次 - $
npx jest async.test --watch
监视改动,变化后执行
- $
1 ) 异步支持功能
async.test.js
// 场景1 callback done
const fetchUser = () => {
setTimeout(() => {
cb('hello');
}, 100)
}
// 测试异步
it('test callback', (done) => {
fetchUser((data) => {
expect(data).toBe('123')
})
})
// 场景2 promise
const userPromise = () => Promise.resolve('hello')
// 测试promise
it('test Promise', () => {
// 这里 return 要加上
return userPromise().then(data => {
expect(data).toBe('hello')
})
})
// 场景3 async/await
it('test async await', async () => {
const data = await userPromise()
expect(data).toBe('hello')
})
// 场景4 expect
it('test expect', async () => {
return expect(userPromise()).resolves.toBe('hello')
})
// 场景5 综合应用
const rejectPromise = () => Promise.reject('error')
it('test with expect reject', async () => {
return expect(rejectPromise()).rejects.toBe('error')
})
以上场景1 不会测试通过,场景2, 3, 4, 5可通过
2 ) Mock 功能
- 为什么要有 Mock?
- 前端需要有网络请求
- 后端依赖数据库等模块
- 局限性:依赖其他的模块
- Mock 解决方案
- 测试替代,将真实代码替换为替代代码
- Mock 的两大功能
- 创建 mock function,在测试中使用,用来测试回调
- 手动 mock,覆盖第三方实现
2.1 jest.fn 的用法
function mockTest(shouldCall, cb) {
if (shouldCall) {
return cb(42)
}
}
it('test with mock function', () => {
const mockCb = jest.fn() // 这个 jest.fn 假的函数实现,类似监听器,收集函数调用信息
mockTest(true, mockCb)
expect(mockCb).toHaveBeenCalled() // 验证是否已经被调用
expect(mockCb).toHaveBeenCalledWith(42) // 验证调用时携带的参数是 42
expect(mockCb).toHaveBeenCalledTimes(1) // 验证调用了多少次
// console.log(mockCb.mock.calls)
// console.log(mockCb.mock.results)
})
it('test mock with implementation', () => {
// const mockCb = jest.fn(x => x * 2)
const mockCb = jest.fn().mockReturnValue(20)
mockTest(true, mockCb)
console.log(mockCb.mock.results) // [ { type: 'return', value: 20 } ]
})
- jest.fn() 可以用于验证事件,比如是否调用了函数,调用函数的参数是什么
- 监听过程是不会被 mockTest 感知到
2.2 手动mock
新建 user.js, 用于测试手动 mock
const axios = require('axios')
module.exports = function getUserName(id) {
const url = `https://xx.com/x/api/user/${id}`
return axios.get(url).then((resp) => {
return resp.data.username // 假设id为1,这里的username 是 wang
})
}
现在我们来测试 user.js 模块, 参考 API
编写 mock.test.js
先看下 mockImplementation 这个API
const getUserName = require('./user')
const axios = reuqire('axios')
jest.mock('axios')
// 这里替换对应的实现,修改结果
axios.get.mockImplementation(() => {
return Promise.resolve({ data: {username: 'Lee'} })
})
// 测试某个api模块
it('test with mock modules', () => {
return getUserName(1).then(name => {
console.log(name);
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toHaveBeenCalledTimes(1)
})
})
再来看下 mockReturnValue
const getUserName = require('./user')
const axios = reuqire('axios')
jest.mock('axios')
// 这里替换对应的实现,修改结果
axios.get.mockReturnValue(Promise.resolve({ data: {username: 'Lee'} }))
// 测试某个api模块
it('test with mock modules', () => {
return getUserName(1).then(name => {
console.log(name); // Lee
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toHaveBeenCalledTimes(1)
})
})
再来看下 mockResolvedValue
const getUserName = require('./user')
const axios = reuqire('axios')
jest.mock('axios')
// 这里替换对应的实现,修改结果
axios.get.mockReturnValue({ data: {username: 'Lee'} })
// 测试某个api模块
it('test with mock modules', () => {
return getUserName(1).then(name => {
console.log(name); // Lee
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toHaveBeenCalledTimes(1)
})
})
- 如果几个测试文件都要 mock axios 的话,这样就比较繁琐,axios 提供一个一劳永逸的方法
- 只需要在需要的地方,创建
__mocks__
的文件夹, 创建__mocks__/axios.js
,将自己要实现的逻辑写在里面 - 比如
__mocks__/axios.js
const axios = {
get: jest.fn(() => Promise.resolve({data: {username: 'mock'}}))
}
module.exports = axios
之后修改 mock.test.js
const getUserName = require('./user')
const axios = reuqire('axios')
// jest.mock('axios')
// axios.get.mockReturnValue({ data: {username: 'Lee'} })
// 测试某个api模块
it('test with mock modules', () => {
return getUserName(1).then(name => {
console.log(name); // Lee
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toHaveBeenCalledTimes(1)
})
})
上面两行被注释,直接使用定义好的 axios.get
即可