单元测试规范

作者: 智联大前端 | 来源:发表于2019-07-05 10:50 被阅读0次

    原则

    单元测试文件必须拥有良好的结构和格式;
    测试用例的分组名称和用例名称必须清晰易懂;
    测试用例必须能描述测试目标的行为;
    优先测试代码逻辑(过程)而非执行结果;
    单元测试的各项覆盖率指标必须在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来模拟或控制其行为
    

    相关文章

      网友评论

        本文标题:单元测试规范

        本文链接:https://www.haomeiwen.com/subject/kdjshctx.html