关于javascript:Jest-单元测试疑难点入门

7次阅读

共计 7760 个字符,预计需要花费 20 分钟才能阅读完成。

文章基调

  • 介绍概念及思考的过程,不提供代码(具体代码写法可参考 jest 官网)
  • 延长:

    • 信息大爆炸时代,各类资源很丰盛,具体教程网上有很多材料
    • 具体不过官网,不反复制作雷同的信息,造成额定的心智累赘
    • 大脑只是搜索引擎,晓得资源从那里找,不负责记录具体做法,节俭内存

测试的几个名称

  • 视觉测试:【测试工具】前端视觉较为多变,故视觉测试的老本较大,普及性不高,但益处在于,能够测试款式信息
  • 单元测试:【测试指标】最小颗粒度的测试,针对单个函数或性能,适宜函数库,根底组件库等的测试
  • 集成测试:【测试指标】模仿用户操作,面向交付的最终后果,针对我的项目的流程
  • TDD(Test Driven Development 测试驱动开发):【方法论】先写测试用例(提出期望值),在写具体的实现办法与函数,使用于单元测试
  • BDD(Behavior Driven Development 行为驱动开发):【方法论】基于集成测试
  • 本文次要介绍 jest(玩笑) 单元测试库

jest 单元测试的原理与局限性

先介绍原理,是心愿让大家晓得其性能边界,能做什么,不能做什么,理解能力范畴

  • jest 运行在 node 端,底层应用实现库是 jsdom,应用 node 模仿一套 dom 环境,模仿的范畴仅局限于 dom 层级构造及操作

  • 【dom 操作】只模仿大部分 dom 通用性能,某些特定性的 dom api 并不反对,如 canvas,video 的媒体性能 api

    • 如果要测试 canvas,video 的媒体 API,须要装置对应的扩大库,能够了解为在 node 端实现浏览器的性能,如图片生成,音视频播放等
    • canvas 扩大,video 相干扩大临时没找到
  • 【css 款式】严格而言,没有 css 款式模仿性能,css 在 jsdom 中只当做纯正的 dom 属性字符串,与 id,class 字符串没有区别

    • 不反对继承,每个 dom 都是独立的个体,没有款式上继承。
    • 仅反对内联款式,无奈辨认 vue 中的款式
    • 不太有用的,解析外链款式的示例
    • 这里有个解决方案,但没有失去官网合并
    • 非内联的款式测试,须要应用视觉测试库

单元测试须要笼罩那些场景?

  • 代码变动

    • 间接运行单元测试即可发现,但如何防止开发者遗记了运行单元测试?
    • 通过增加 cicd 流程解决,提交 merge request 申请时,触发单元测试,运行失败,则主动回绝合并申请,并执行 node 命令发送音讯揭示
    • gitlab ci 相干配置会在文章开端介绍
  • 新增代码

    • 新增的函数或者性能,运行旧的单元测试不会笼罩到,如何揭示开发者笼罩新增的这部分代码?
    • 通过配置测试覆盖率行数 100% 解决,达不到指标,则视为测试不通过,防止新增代码的脱漏。切实无奈笼罩的分支或函数,怎么解决?
    • 通过配置「疏忽备注 / istanbul ignore next /」,放弃某文件的百分百覆盖率测试
    • 后续有工夫也能够通过全局搜寻这些疏忽配置,来一一笼罩测试,起到标记的作用

      coverageThreshold: {
       './src/common/js/*.js': {
         branches: 80, // 笼罩代码逻辑分支的百分比
         functions: 80, // 笼罩函数的百分比
         lines: 80, // 笼罩代码行的百分比
         statements: -10 // 如果有超过 10 个未笼罩的语句,则 jest 将失败
       }
      },
  • 新增文件,是否脱漏测试

    • 个别状况下,单元测试只会跑单元测试文件,新增的代码文件没有对应的测试文件,会呈现漏测的状况
    • 通过 collectCoverageFrom 参数指定须要笼罩的文件夹,当该文件夹中的文件没有对应的测试用例,会当作覆盖率 0 解决,起到新文件漏测揭示作用

      // 从那些文件夹中生成覆盖率信息,包含未设置对其编写测试用例的文件,解决脱漏新文件的测试笼罩问题
      collectCoverageFrom: ['./src/common/js/*.{js,jsx}',
        './src/components/**/*.{js,vue}',
      ],
  • 非凡场景(教训的价值)

    • 局部函数,在失常状况下运行是没有问题的,仅在非凡的状况下才会报错,如简略的加法运算,放在小数中就会呈现计算误差,0.1 + 0.2 = 0.30000000000000004
    • 这些非凡场景的笼罩,只能靠一线开发人员在理论工作中记录,须要工夫的积攒
    • 这是程序员教训的价值,也是少有的,值得传承的局部

单元测试疏忽原理

jest 收集覆盖率底层应用的是 istanbul 库(istanbul:伊斯坦布尔,胜产地毯,地毯用于笼罩),以下疏忽格局都是 istanbul 库的性能

  • 疏忽本文件,放在文件顶部 / istanbul ignore file /
  • 疏忽一个函数, 一块分支逻辑或者一行代码,放在函数顶部 / istanbul ignore next /
  • 疏忽函数参数默认值function getWeekRange(/* istanbul ignore next */ date = new Date()) {
  • 具体疏忽规定可查看 istanbul github 介绍

编写测试用例的正确姿态

以对性能的冀望及定位作为出发点,而不是代码,一开始应先思考该函数或工具库须要起到的性能,而不应该一开始就看代码

  • 先列举你冀望的,该组件或者函数的性能,用文本写进去,这也是 test('检测点击事件') 中形容的作用,告知别人这个测试用例的目标
  • 编写相应的测试用例
  • 对不满足测试用例的代码进行批改
  • 察看代码覆盖率,笼罩所有代码行

增加 jest 全局自定义函数

  • 如果某测试函数的呈现频率比拟高,能够思考对齐进行复用,写成一个预加载文件,在每个测试文件执行前,加载该文件
  • 如获取 dom 款式的原始代码比拟繁琐,wrapper.element.style.height,且 element 并没有失去官网裸露,属于外部变量
  • 能够通过增加配置文件,编写 styles 全局办法,通过函数的形式获取 style 数据,与 classes 等办法放弃对立

    // jest.config.js 设置前置运行文件,在每个测试文件执行前,会运行该文件,实现增加某些全局办法的作用
    setupFilesAfterEnv: ['./jest.setup.js'],
    // ./jest.setup.js
    import {shallowMount} from '@vue/test-utils'
    
    // 向全局 wrapper 挂载通用函数 styles,返回该元素的内联款式(因为 jsdom 只反对内联款式,不反对检测 class 中的款式),或某内联款式的值
    function addStylesFun() {
      // 生成一个长期组件,获取 vueWrapper 及 domWrapper 实例,挂载 style 办法
      const vueWrapper = shallowMount({template: '<div>componentForCreateWrapper</div>'})
      const domWrapper = vueWrapper.find('div')
    
      vueWrapper.__proto__.styles = function(styleName) {return styleName ? this.element.style[styleName] : this.element.style
      }
    
      domWrapper.__proto__.styles = function(styleName) {return styleName ? this.element.style[styleName] : this.element.style
      }
    }
    addStylesFun()
    

钩子函数

相似于 vue router 外面的守卫函数,在进入前后执行钩子函数

  • 解决有状态函数的数据存储问题,防止执行每一个测试用例时,反复编写代码筹备数据
  • beforeAll、afterAll

    • 写在单元测试文件最内部,则代表在该函数在文件执行前、后被执行一次
    • 写在测试组 describe 最外层,代表该函数在测试组执行前、后被执行一次
  • beforeEach、afterEach

    • 每个测试用例(test)前后执行一次

疾速单元测试技巧

跳过已测试胜利且源码没产生过变更的用例,不再多余执行

  • 第一步,jest –watchAll 测试文件产生过变动,则主动执行测试

    • 只能在 package script 命令中增加该参数,在 npm 命令后执行不失效
    • 源码变更,或单元测试文件变更,都会触发
  • 第二步,按下 f(只执行谬误的用例)

    • 毛病在于,不能监控已执行胜利的单元测试的变动,以及对应源码的变动,(即之前胜利过的都会被疏忽,不论新的变动,是否产生了谬误)
    • 源码变更,或单元测试文件变更,都会触发
    • 可通过重复按下 f 来切换全局遍历
  • 第三步,再按下 o (只执行源码产生过变动的文件的测试用例)

    • 等价于 jest –watch
    • 只监听 git 中,未提交到暂存区的文件,一旦提交了 stash,则不再触发
    • 即便该文件中存在失败的测试用例,也会被疏忽
    • 按下 a 来跑全副文件的测试用例,即 a 与 o 的切换
    • 底层是通读取 .git 文件夹的内容进行文件辨别,故依赖 git 的存在
  • 按下 w 能够显示菜单,查看 watch 的选项
    个别状况下,汇合 o 与 f 应用,先 o(疏忽没变动的文件,当咱们改变该文件时,将会被监听。再重复按下 f,只监听谬误的用例)


jest 报告阐明

  • 鼠标悬浮对应图表,即可显示对应提醒
  • 「5x」示意在测试中这条语句执行了 5 次
  • 「I」是测试用例 if 条件未进入,即没有 if 为真的测试用例
  • 「E」是测试用例没有测试 if 条件为 false 时的状况

    • 即测试用例中 if 条件始终都是 true,得写一个 if 条件为 false 的测试用例,即不走进 if 条件外面的代码,这个 E 才会隐没

模仿函数,不是模仿数据的函数

  • 只是模仿函数(Function、jest.fn()),并不是像 mockjs 一样,生成模仿数据的函数
  • 作用:

    • 检测该函数被执行过多少次
    • 检测该函数被执行时的 this 指向
    • 检测执行时的入参
    • 检测执行后的返回值
  • 笼罩模仿第三方函数

    • 笼罩 axios 函数,防止真正发动接口,定制特定的返回值 jest.mock('axios'); axios.get.mockResolvedValue(resp);
    • 外面没有魔法,也没有私下适配,只是单纯的函数重载。相当于 axios.get = ()=> resp 重写了该办法
  • 终极办法,笼罩整个第三方库

    • 编写替身文件,在应用 import 导入时,导入的是替身文件
    • 也能够通过 jest.requireActual(‘../foo-bar-baz’) 来强制设置导入的是实在的文件,不应用替身文件

计时器模仿

  • 复写 setTimeout 计时器,能够跳过指定时长,缩短单元测试运行时长

测试快照

  • 快照,即数据正本,即检测「以后数据」是否与「旧有数据正本」雷同,相似于 JSON.stringify(),进行数据的序列化记录
  • 利用场景

    • 限度配置文件的变更
    • 检测 dom 构造的比拟,某函数的变更,是否影响 dom 构造
    • 总体而言,用在大数据的比拟操作,防止将数据写死在单元测试文件中

其余疑难杂症

  • 别名与等价的办法

    • it 是 test 的别名,两者等价
    • toBeTruthy !== toBe(true)、toBeFalsy !== toBe(false),toBe(true) 更严格,toBeTruthy 是强转为 boolean 后,是否为真
    • skip 跳过某测试用例,比正文更优雅describe.skip('测试自定义指令',xxx)`test.skip(‘ 测试自定义指令 ’,xxx)`
  • jest toBe,外部应用 Object.is 进行比拟

    • 与 === 的区别是,除了 NaN,+0 和 -0 之外,其行为与三等号于运算符雷同
  • 解决小数点浮点数计算误差问题 toBeCloseTo
  • 异步测试,通过 .resolves / .rejects 强制校验 promise 走特定分支

    test('the data is peanut butter', () => {return expect(fetchData()).resolves.toBe('peanut butter');
    });
  • 解决默认参数为 new Date 的笼罩问题

    test('以后月,测试参数 new Date 默认值', () => {
      // 覆写 new Date 的值,模仿为 2022/01/23 17:13:03,解决默认参数为 new Date 时,无奈笼罩的问题
      const mockDate = new Date(1642938133000)
      const spyDate = jest
        .spyOn(global, 'Date') // 即监听 global.Date 变量
        .mockImplementationOnce(() => {spyDate.mockRestore() // 须要在第一次执行后,马上打消该 mock,防止后续影响后续 new Date
          return mockDate
        })
      let [starTime, endTime] = getMonthRange()
      expect(starTime).toBe(1640966400000) // 2022/01/01 00:00:00
      expect(endTime).toBe(1643644799000) // 2022/01/31 23:59:59
    })
    • 等价于应用原生语法写

      const OriginDate = global.Date
      Date = jest.fn(() => {
        Date = OriginDate
        return new OriginDate(1642938133000)
      })
    • 应用最新语法

      beforeAll(() => {jest.useFakeTimers('modern')
          jest.setSystemTime(new Date(1466424490000)) // 因为 Vue Test Utils 中应用的 jest 是 24.9 的版本,没有该函数
        })
      
        afterEach(() => {jest.restoreAllMocks()
        })
  • 匹配测试,及应用多个批次的数据,进行跑同一个测试用例

    describe.each([[1, 1, 2], // 每一行是代表运行一次测试用例
      [1, 2, 3], // 每一行中的参数,是运行该次测试用例是用到的数据,前两个是参数,第三个是测试期望值
      [2, 1, 3],
    ])('.add(%i, %i)', // 设置 describe 的题目,%i 是 print 的变量参数
     (a, b, expected) => {test(`returns ${expected}`, () => {expect(a + b).toBe(expected);
      });
    });

gitlab-ci 单元测试相干配置

  • 在发动 merge 合并申请时,触发 ci 执行单元测试
  • 当单元测试失败,执行 node 文件,发送飞书信息,飞书信息中,包含该次 merge 申请的链接,能够点击该链接,疾速定位到单元测试 job,查看问题

    stages:
    - merge-unit-test
    - merge-unit-test-fail-callback
    - other-test
    
    # merge 申请时执行的 job
    step-merge:
    stage: merge-unit-test
    
    # 应用的 gitlab runner
    tags: [front-end]
    
    # 仅在提出代码合并申请时执行
    only: [merge_requests]
    
    # 排除特定分支的代码合并申请,即在特定分支的代码合并申请时,不执行该 job
    except:
      variables:
        - $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "qa"
    
    # 运行的命令
    script:
      - npm install --registry=https://registry.npm.taobao.org # 装置依赖
      # 2>&1 规范谬误定向到规范输入
      # Linux tee 命令用于读取规范输出的数据,并将其内容输入成文件。- npm run test 2>&1 | tee ci-merge-unit-test.log # 执行单元测试,并将在控制台输入的信息,保留在 ci-merge-unit-test.log 文件中,以便后续剖析
      - echo 'merge-unit-test-finish'
    
    # 定义往下一个 job 须要传递的材料
    artifacts:
      when: on_failure # 默认状况下,只会在 success 保留,能够通过这个标识符进行配置
      paths:  # 定义须要传递的文件
        - ci-merge-unit-test.log
    
    # merge 检测失败时执行的 node 命令
    step-merge-unit-test-fail-callback:
    stage: merge-unit-test-fail-callback
    
    # 当上一个 job 执行失败时,才会触发
    when: on_failure
    
    tags: [front-end]
    
    only: [merge_requests]
    
    except:
      variables:
        - $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "qa"
    
    script:
      - node ci-merge-unit-test-fail-callback.js $CI_PROJECT_NAME $CI_JOB_ID # 执行 node 脚本,进行飞书告诉,并携带对应链接,进行疾速定位
    
  • ci-merge-unit-test-fail-callback.js.js

    const fs = require('fs')
    const path = require('path')
    const https = require('https')
    const projectName = process.argv[2] // 我的项目名
    const jobsId = process.argv[3] // 执行的 ci 工作 id
    
    const logsMainMsg = fs.readFileSync(path.join(__dirname, 'ci-merge-unit-test.log'))
    .toString()
    .split('\n')
    .filter(line => line[line.length - 1] !== '|' && line.indexOf('PASS') !== 0) // 过滤不关注的信息
    .join('\n')
    
    const data = JSON.stringify({
    msg_type: 'post',
    content: {
      post: {
        zh_cn: {
          content: [
            [
              {
                tag: 'a',
                text: 'gitlab merge 单元测试',
                href: `https://xxx/fe-team/${projectName}/-/jobs/${Number(jobsId) - 1}`
              },
              {
                tag: 'text',
                text: ` 运行失败 \r\n${logsMainMsg}`
              }
            ]
          ]
        }
      }
    }
    })
    
    const req = https.request({
    hostname: 'open.feishu.cn',
    port: 443,
    path: '/open-apis/bot/v2/hook/xxx',
    method: 'POST',
    headers: {'Content-Type': 'application/json'}
    }, res => {console.log(`statusCode: ${res.statusCode}`)
    res.on('data', d => process.stdout.write(d))
    })
    req.on('error', error => console.error(error))
    req.write(data)
    req.end()
    

感激

  • 近期文章产出少,事件太多,也懒了
  • 感激网友的挂念和督促,被人牵挂的感觉真好

正文完
 0