原则
单元测试文件必须拥有良好的结构和格式;
测试用例的分组名称和用例名称必须清晰易懂;
测试用例必须能描述测试目标的行为;
优先测试代码逻辑 (过程) 而非执行结果;
单元测试的各项覆盖率指标必须在 95% 以上;
技术
Jest:https://facebook.github.io/jest
结构
编写单元测试所涉及的文件应存放于以下两个目录:
mocks/: 模拟文件目录
[name].mock.JSON:[例] 单个模拟文件
tests/: 单元测试目录
[target].test.JS:[例] 单个单元测试文件,[target]与目标文件名保持一致, 当目标文件名为 index 时, 采用其上层目录或模块名.
[target].test.JS 文件
应按照如下结构编写测试文件, 注意其中的空行:
- /* eslint global-require: 0 */
- const thirdPartyModule = require('thrid-party-module')
- describe('@zpfe/module-name' () => {
- const mocks = {}
- beforeAll(() => {})
- beforeEach(() => {})
- test('描述行为', () => {
- mocks.fake.mockReturnValue('控制模拟行为的代码置于最上方')
- const target = require('../target.js')
- const result = target.foo('执行目标待测功能')
- expcet(result).toBe('断言置于最下方')
- })
- })
保证每个 describe 内部只有 mock 对象, 生命周期钩子函数和 test 函数, 将模拟对象都添加到 mocks 对象的适当位置, 将初始化操作都添加到适当的生命周期函数中.
mocks 对象
常量 mocks 的结构如下:
- const mocks = {
- zpfe: {
- // @zpfe 模块, 若有, 将包名转换为驼峰式以便访问, 比如: koaMiddleware
- log: {
- info: jest.fn()
- }
- },
- dependencies: {
- thirdPartyModule1: {
- // 第三方依赖模块, 若有
- },
- files: {
- // 本地依赖文件
- router: jest.fn()
- },
- others: {
- // 公共假对象
- ctx: jest.fn()
- }
- }
请注意, mocks 对象的价值在于保存模拟依赖项及部分复用对象, 请勿添加不涉及模拟也没有被复用的内容.
生命周期函数
在 beforeAll 中设置依赖模拟, 比如:
- beforeAll(() => {
- jest.mock('@zpfe/log', () => mocks.zpfe.log)
- jest.mock('../router.js', () => mocks.files.router)
- jest.spyOn(console, 'log')
- })
在 beforeEach 中进行每个单元测试运行前需要的重置工作, 比如:
- beforeEach(() => {
- process.env.NODE_ENV = 'production'
- })
describe 函数
若模块包含多个文件, 则每个文件对应专门的测试文件, 其 describe 应这样写:
describe('@zpfe/module-name: file-name' () => {})
目标对象
提倡在每个 test 函数中 require 目标文件, 若综合评估之后, 能确定将 require 目标文件的代码提取到生命周期钩子函数中也不会产生干扰或混乱, 则可以考虑提取, 比如:
- describe('@zpfe/module-name' () => {
- let moduleName
- beforeEach(() => {
- moduleName = require('../target.js')
- })
- })
test 函数
请用空行分隔 test 函数内不同目的的代码块(比如模拟, 执行目标, 和断言).
请勿在测试中编写 try...catch..., 应明确断言是否抛出异常, 并根据需要断言抛出的错误信息及日志记录情况.
命名
describe 表示分组, 其名称应属于下列几种情况之一:
模块名, 比如:@zpfe/module-name
组件名, 比如: a-input
隶属于模块或组件的文件名, 比如: a-input/nativa-control
功能名, 比如: props
条件名, 比如: 当 NODE_ENV = production 时
test 表示测试用例, 其名称应当明确表示其行为, 比如: 当 disabled 属性被设置为非 Boolean 类型时, 抛出异常. 不允许将 describe 的命名规则应用到 test.
良好的命名有助于组织测试用例, 使其更能充当文档之用. 当某个测试用例失败时, 良好的结构和命名能让读者快速了解其影响范围, 比如:
[FAILED] a-input> props -> disabled -> 当传入非 Boolean 类型的值时, 抛出异常
模拟
请查阅 Jest 文档, 以详细了解 Jest 所提供的各类模拟 API.
临时替换默认实现
若在 mocks 对象中初始化了实现, 又需要在测试用例当中临时修改其实现, 可以这样做:
- const mocks = {
- others: {
- foo: jest.fn(() => 'foo')
- }
- }
- test('demo', () => {
- mocks.others.foo.mockImplementationOnce(() => 'bar')
- })
mockImementOnce 会临时修改默认实现, 且只生效一次, 故不会影响其他测试用例.
若需要在测试用例当中临时修改模拟函数的实现, 且模拟函数会被多次调用, 就应该使用另外一种方式实现, 比如:
- const mocks = {
- others: {
- foo: jest.fn()
- }
- }
- beforeEach(() => {
- mocks.others.foo.mockImplementation(() => 'foo')
- })
- test('demo', () => {
- mocks.others.foo.mockImplementation(() => 'bar')
- })
即在 mocks 对象中只定义模拟函数, 不定义具体实现, 在 beforeEach 钩子函数中定义具体实现, 使得每个测试用例都会重新初始化该实现, 接着在具体测试用力中使用 mockImementation 彻底替换掉默认实现.
断言
请查阅 Jest 文档, 以详细了解 Jest 所提供的各类断言 API.
断言参数
若需要断言调用函数时的参数传递, 可使用:
expect(mocks.zpfe.log.info).toHaveBeenCalledWith('观察 C ZooKeeper 客户端')
若需要部分匹配参数, 可使用:
expect(mocks.zpfe.log.info).toHaveBeenCalledWith(expect.stringContaining('观察'), expect.objectContaining({ key: 'value' }))
调试
在 VS Code 中, 打开测试文件, 选中调试配置[调试 Jest 测试] , 按[F5] 即可.
vue 组件测试
技术
@vue/test-utils:https://vue-test-utils.vuejs.org
结构
单元测试文件在 tests 目录内的组织形式应与目标文件在 src 目录保持一致, 并按照如下顺序结构组织组件的单元测试文件:
- describe('组件: a-component-name', () => {
- const mocks = {}
- beforeAll()
- // 仅针对 props 定义进行基础测试, 不测试 props 如何使用
- describe('props', () => {
- describe('prop-name', () => {
- test('类型应为 xxx')
- test('默认值应为 xxx')
- test('有效性校验')
- })
- })
- // 仅针对可被用户使用的嵌套组件族进行嵌套校验测试
- describe('受限嵌套', () => {
- test('当父组件不为 xxx 时, 抛出异常')
- test('当子组件不为 xxx 时, 抛出异常')
- })
- // 仅针对 slots 渲染位置进行基础测试
- describe('slots', () => {
- test('default')
- test('named-slot')
- })
- // 根据实际情况, 结合 props 和 slots 进行各种场景下的渲染测试
- describe('render', () => {
- test('使用 prop-name 来渲染 xxx')
- })
- // 测试所有公开方法, 不测试私有方法
- describe('methods', () => {
- describe('method-name', () => {
- test('行为')
- })
- })
- // 触发并测试所有事件是否正常触发
- // 若 props 中包含 value, 则 events 中必须包含 input
- describe('events', () => {
- describe('event-name', () => {
- test('当 xxx 时, 触发此事件')
- })
- })
- // 测试 UI 交互是否能正常响应(忽略与 events 测试雷同, 则可忽略)
- describe('交互', () => {
- test('当点击 xxx 时, 如此这般')
- })
- }
挂载组件
按照如下规则挂载组件:
在挂载时传递 props;
挂载产生的对象应命名为 e;
若组件需要使用原生 DOM 方法, 请启用 attachToDocument;
比如:
- const target = mount(ComponentName, {
- propsData: {
- foo: 'bar'
- },
- attachToDocument: true
- })
组件依赖关系
除非互相依赖的组件之间定义了嵌套校验, 否则优先考虑模拟子组件来进行父组件的测试. 比如:
- const target = mount(APaginationWithJumper, {
- stubs: {
- 'dependent-component': true
- }
- }
- // 通过 target.find('dependent-component-stub').vm 来模拟或控制其行为
来源: http://www.jianshu.com/p/05287f88ebed