前端单元测试调研

968 阅读8分钟

英国Just Eat的首席UI工程师Ashley Watson-Nolan做过这样一个调查,16年有48.32%的前端开发者没有做过任何前端代码测试,这个数字到18年下降了4.32%, 到19年则下降到了21%,可见前端测试这一环节在前端开发者中的普及率明显上升,下图是18年和19年该调查中开发者对测试工具的选择和使用情况汇总 image.pngimage.png

什么是单元测试


单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证,对于JavaScript来说,通常也是针对函数、对象和模块的测试。 单元测试的特性--

  • 只要输入不变,必定返回同样的输出
  • 不依赖外部系统
  • 运行速度很快(毫秒级别)
  • 不造成测试环境的脏数据
  • 可重复运行

一个软件越容易写单元测试,也就表明它的模块化结构越好,各模块之间的耦合越弱。React的组件化和函数式编程,天生就适合进行单元测试

为什么要进行单元测试


没有单测的情况:

  • 质量主要依赖自己保障
  • 开发依赖手工测试,耗时耗力
  • 代码更新频繁时,每次迭代新功能难保证已有核心功能测试完整性
  • 对于复杂的功能,回归困难,工作量大

单元测试的优点:

  • 提高代码质量:单元测试可以帮助我们反思模块划分的合理性,如果一个单元测试写得逻辑非常复杂、或者说一个函数复杂到无法写单测,那就说明模块的抽象有问题。
  • 提升可维护性:单元测试使得系统具备更好的可维护性,可读性,便于多人同时开发。
  • 快速熟悉代码:对于团队的新人来说,单元测试其实是很好的"文档",每个case 都能详细的反映代码包含的具体功能和业务
  • 效率提升:快速定位问题,提升调试效率,每次跑完单测查看不通过的case就能快速定位问题
  • 放心重构:单元测试使得我们可以放心修改、重构业务代码,而不用担心修改某处代码后带来的副作用。

单元测试位于测试金字塔的最底层,越向上反馈的时间越长,实现的成本也越高。 image.png

单元测试实践的主要问题


通常会面临四大问题:

  • 不愿做:程序员没有单元测试习惯
  • 没时间:编写测试代码需要耗费一定的时间,项目的周期可能不允许
  • 做不了:代码具有较高的耦合性,使单元测试难以进行
  • 做不好:测试效果不能令人满意。实现高标准的测试覆盖很困难

如何应对解决以上问题


  • 不愿做:将单元测试覆盖率纳入代码质量指标和研发流程,作为保障代码质量的一个硬性指标来推进,磨刀不误砍柴工,最终会形成一个良性循环
  • 没时间:可以优先从基础组件库来推进单测的落地;循序渐进、多方协商、TL支持、自己争取
  • 做不了:优先在新需求中实践单元测试,而耦合度较高、历史包袱沉重的代码,应该从重构的层面解决问题
  • 做不好:"先做" 带动 "后做", 制定统一规范,多读多写多交流,CR评估测试有效性等

整体思路--

  • 循序渐进,优先从基础组件库落地推进执行,可以把单测作为组件入组件库的一个硬性标准
  • 集成到脚手架,使用git-commit hook 让单元测试流程化
  • 重构代码必须加入单元测试
  • 优先做非UI部分的单元测试
  • 业务场景多、复杂或经常回归的场景可以多写些端到端测试

开发模式


  • TDD: (Testing Driven Development)测试驱动开发 ,强调的是一种开发方式,以测试来驱动整个项目,它依赖于非常短的开发周期的重复:  需求被转化为具体的测试用例,通过不断的代码改进,通过测试用例,以"小步快跑" "持续循环" 的方式来完成整个项目。
    • 写一个测试
    • 运行这个测试,看到预期的失败
    • 编写尽可能少的业务代码,让测试通过
    • 重构代码
    • 不断重复以上过程

image.png image.png

  • BDD: (Behavior Driven Development)行为驱动测试,BDD建议针对行为进行测试,我们不考虑如何实现代码,取而代之的是我们花时间考虑场景是什么,会有什么行为,针对行为 代码应该有什么反应。鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作

可以把BDD看作是在需求与TDD之间架起一座桥梁,它将需求进一步场景化,更具体的描述系统应该满足哪些行为和场景,让TDD的输入更优雅、更可靠。

单元测试框架对比


  • 提供测试框架(Jest,Mocha, Jasmine, Cucumber)
  • 提供断言(Jest,Chai, Jasmine, Unexpected)
  • 生成,展示测试结果(Jest,Mocha, Jasmine, Karma)
  • 快照测试(Jest, Ava)
  • 提供仿真(Jest,Sinon, Jasmine, enzyme, testdouble)
  • 生成测试覆盖率报告(Jest,Istanbul, Blanket)
  • 提供类浏览器环境(Puppeteer, Nightwatch, Phantom, Casper)

image.png

推荐测试工具:Jest + Enzyme


Jest是Facebook开源的一个前端测试框架,主要用于React和React Native的测试,已被集成在create-react-app中。集成了断言库、mock库、测试覆盖率统计功能、 Snapshot 机制。Jest 几乎可以做0配置使用。

Enzyme是Airbnb开源的React测试工具库,它扩展了React的TestUtils,提供了一套简洁强大的 API,并内置Cheerio。 我们可以用Enzyme渲染react组件,遍历和操作react组件的输出,Enzyme提供了类Jquery风格简洁的API, 使得dom操作变得十分友好。在开源社区有超高人气,同时也获得了React 官方的推荐。

测试环境搭建

 npm install jest enzyme babel-jest enzyme-adapter-react-16 enzyme-to-json --save-dev
.jest.js文件

module.exports = {
  setupFiles: ['./test/setup.js'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 
  testPathIgnorePatterns: [
    '/node_modules/',
  ],
  testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$',
  collectCoverage: false,
  collectCoverageFrom: [
    'src/components/**/*.{ts,tsx,js,jsx}',
  ],
  moduleNameMapper: {
   '\\.(css|less)$': 'identity-obj-proxy',
  },
  transform: {
    "^.+\\.js$": "babel-jest"
  },
};
  • setupFiles:配置文件,在运行测试案例代码之前,Jest会先运行这里的配置文件来初始化指定的测试环境
  • moduleFileExtensions:代表支持加载的文件名
  • testPathIgnorePatterns:用正则来匹配不用测试的文件
  • testRegex:正则表示的测试文件,测试文件的格式为xxx.test.js
  • collectCoverage:是否生成测试覆盖报告
  • collectCoverageFrom:生成测试覆盖报告时检测的覆盖文件
  • moduleNameMapper:代表需要被Mock的资源名称
  • transform:用babel-jest来编译文件,生成ES6/7的语法

enzyme


三种渲染

  • shallow:浅渲染,是对官方的Shallow Renderer的封装。将组件渲染成虚拟DOM对象,只会渲染第一层,子组件将不会被渲染出来,使得效率非常高。不需要DOM环境, 并可以使用jQuery的方式访问组件的信息
  • render:静态渲染,它将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构
  • mount:完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期。用到了jsdom来模拟浏览器环境

常用方法

  • simulate(event, mock)  模拟事件,用来触发事件,event为事件名称,mock为一个event object
  • instance()  返回组件的实例
  • find(selector)  根据选择器查找节点,selector可以是CSS中的选择器,或者是组件的构造函数,组件的display name等
  • at(index)   返回一个渲染过的对象
  • get(index)  返回一个react node,要测试它,需要重新渲染
  • contains(nodeOrNodes)  当前对象是否包含参数重点 node,参数类型为react对象或对象数组
  • text()  返回当前组件的文本内容
  • html() 返回当前组件的HTML代码形式
  • props() 返回根组件的所有属性
  • prop(key) 返回根组件的指定属性
  • state() 返回根组件的状态
  • setState(nextState)  设置根组件的状态
  • setProps(nextProps)  设置根组件的属性

jest


常见断言

钩子

执行Jest测试用例之前需要做一些配置,在用例执行完做一些清除操作,那你需要了解下面4个API

mock

使用mock函数可以轻松的模拟代码之间的依赖,可以通过fn或spyOn来mock某个具体的函数;通过mock来模拟某个模块。具体的API可以看mock-function-api

异步测试      

  • return promise        

当对Promise进行测试时,一定要在断言之前加一个return,不然没有等到Promise的返回,测试函数就会结束。可以使用.promises/.rejects对返回的值进行获取,或者使用then/catch方法进行判断。

  • async/await

使用async不用进行return返回,并且要使用try/catch来对异常进行捕获。

Snapshot Testing

快照测试可以用于保证界面不出现异常变化。 快照会生成一个组件的UI结构,并用字符串的形式存放在__snapshots__文件里,通过比较两个字符串来判断UI是否改变,因为是字符串比较,所以性能很高。 要使用快照功能,需要引入react-test-renderer库,使用其中的renderer方法,jest在执行的时候如果发现toMatchSnapshot方法,会在同级目录下生成一个__snapshots文件夹用来存放快照文件,以后每次测试的时候都会和第一次生成的快照进行比较。可以使用jest --updateSnapshot来更新快照文件。

import React from 'react'
import renderer from 'react-test-renderer'

it('renders correctly', () => {
  const tree = renderer
  .create(<TodoList {...props} />)
          .toJSON();

  expect(tree).toMatchSnapshot();
});

单测覆盖率

Jest集成了Istanbul这个代码覆盖工具,  提供了生成测试覆盖率报告的命令 --coverage

四个测量维度--

  • 行覆盖率(line coverage):是否测试用例的每一行都执行了
  • 函数覆盖率(function coverage):是否测试用例的每一个函数都调用了
  • 分支覆盖率(branch coverage):是否测试用例的每个if代码块都执行了
  • 语句覆盖率(statement coverage):是否测试用例的每个语句都执行了

image.png


参考资料