metroluffy / blog

用于记录平时开发中所遇到问题的解决方法、笔记等
9 stars 1 forks source link

使用Jest进行单元测试 #37

Open metroluffy opened 3 years ago

metroluffy commented 3 years ago

为何要写单测

单元测试(Unit Test)作为持续集成实现中的一环,位于金字塔模型的底部,目标是证明代码的某个单元(被测试的主体)能按照预期工作,这样我们在开发过程早期就能发现问题。

一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

说白话,就是确保你代码输出的是你想要的结果。

直接来看个例子。

项目使用了MomentJS处理时间相关的Case,前阵子Moment.js 宣布停止开发,进入维护状态,加上又有打包体积的优化需求,Day.js无疑是更好的选择,遂进行替换。

两者API基本相同,但某些Case下还是略有不同,比如:

// 返回某年第几周的首尾日期(MM/DD)
function transforWeekInterval(year, weekNums = 1) {
  const now = moment(year);
  // 这里有一些边界的校验,略过...
  now.add(weekNums - 1, 'w');
  return {
    start: now.startOf('isoWeek').format('MM/DD'),
    end: now.endOf('isoWeek').format('MM/DD')
  }
}
// output: transforWeekInterval('2020') -> {end: "01/05", start: "12/30"}

针对这个函数有以下测试用例:

// Test Case 0
it('should transforWeekInterval return correct value in 2021 w10', () => {
  expect(transforWeekInterval('2021', 10)).toEqual({end: "03/07", start: "03/01"});
});
// Test Case 1
it('should transforWeekInterval return correct value in the first week 2020', () => {
  expect(transforWeekInterval('2020')).toEqual({end: "01/05", start: "12/30"});
});

当简单地将moment替换成day.js后,发现测试用例Case 1未通过,

expected: {end: "01/05", start: "12/30"}
received: {end: "01/01", start: "01/01"}

// 这里的修复
// dayjs().startOf('isoWeek') -> dayjs().isoWeekday(weekNums -1)

如此,重构早期便可以发现错误,及时处理,否则在后面的测试环节需要投入更多的人力。也一定程度说明了单测的重要性,放心重构。

配置

Jest 是由 Facebook 维护的 JavaScript 测试框架,其重点是简单性。它适用于以下項目:Babel、TypeScript、Node.js、React、Angular 和Vue.js。

为什么使用Jest,不赘述,简单易用、代码覆盖率,也有比较清晰的报错信息。这里给到一份React项目常用的配置,对应的配置项在文档中可找到说明。

// In package.json
"jest": {
    // 测试入口
    "roots": [
      "<rootDir>/src"
    ],
    // 覆盖率收集
    "collectCoverageFrom": [
      "src/**/*.{js,jsx}",
    ],
    // 初始化的一些配置,路径数组
    // 在jest inital之前执行,比setupFilesAfterEnv更早,例如可以设置全局变量到global
    "setupFiles": [
      // jsdom,不涉及dom可不引入
      "react-app-polyfill/jsdom"
    ],
    // 类似,在jest inital之后执行,这一步能拿到jest的api进行扩展(故名AfterEnv)
    // 可以执行例如引入enzyme配置等
    "setupFilesAfterEnv": [
      "<rootDir>/src/setupTests.js"
    ],
    // 测试匹配文件
    "testMatch": [
      "<rootDir>/src/**/__tests__/**/*.{spec,test}.{js,jsx}",
      "<rootDir>/src/**/*.{spec,test}.{js,jsx}"
    ],
    "testEnvironment": "jest-environment-jsdom-fourteen",
    // 转译, babel插件引入
    "transform": {
      "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns": [
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
      "^.+\\.module\\.(css|sass|scss)$"
    ],
    "modulePaths": [],
    "moduleNameMapper": {
      "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
      // alias, 与Webpack一致
      "@images(.*)$": "<rootDir>/src/images/$1",
      "@components(.*)$": "<rootDir>/src/components/$1",
      "@utils(.*)$": "<rootDir>/src/utils/$1",
      "@service(.*)$": "<rootDir>/src/service/$1"
    },
    "modulePathIgnorePatterns": [
      // test 中使用的mock文件,可忽略
      "__mocks__"
    ],
    "moduleFileExtensions": [
      "web.js",
      "js",
      "json",
      "web.jsx",
      "jsx",
      "node"
    ]
  },

以上是JavaScript的配置,配置的目录结构测试文件位于src/**__tests__/,好处是贴近业务代码,也可以统一维护在根目录的test目录下。TS也差不多。可见配置上简单易懂,基本上可以说是开箱即用了。

Jest配置可以单独维护在具体的文件中,然后在cli中指定,也可以放在package.json。React的create-react-app则提供了开箱即用的jest配置和example。略微有坑点可能是Babel转译这块,Jest 无法直接解析 JSX 的语法,好在常见的错误都可以搜索得到。

代码覆盖率

覆盖率的意义在于分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点。一般地,也作为测试考核的一个节点,但不应该成为重点。第二一个是增量覆盖率,可以使用istanbul.js等来统计。

image

在Jest cli中添加--coverage即可生成代码覆盖率报告,或者在配置文件中设置collectCoverage: true

运行测试完毕后结果如下:

811606139180_ pic

同时也会在根目录下生成coverage目录,里面是可视化的web文件,具体到覆盖行/分支,也有代码视图: image

开发中要实现比较完好的单测覆盖,目测会增加20%-30%的开发量,所以需求评估时注意多给点时间...

个人理解测试既能保障产品交付,也可以提升代码质量,编写可测试代码应该是基本能力。

以上。本文仅作为开发记录,也是测试入门文章。 image