Jest

基于 Jest 的单元测试

Jest 是由 Facebook 开源出来的一个测试框架,它集成了断言库、mock、快照测试、覆盖率报告等功能。React 官方文档中提及,Jest 是 Facebook 官方使用的组件测试库。不过 React 也并不排斥其他测试框架,你也可以根据自己的喜好或者团队的统一选择譬如 Mocha、AVA 等测试框架。本部分我们就介绍如何从零开始为项目添加基于 Jest 的测试用例。

环境搭建

依照惯例,我们先使用 create-react-app 命令创建项目;在 package.json 项目文件创建完毕之后即使用 npm 命令安装所需要的依赖:

npm install --save-dev jest

为了方便在 npm 中使用 jest 命令行工具来运行所有的测试用例,我们需要在 package.json 文件中添加如下脚本配置:

"scripts": {
  "test": "jest"
},

此外,为了方便对我们使用 ES2015 与 JSX 语法的组件或者类进行测试,我们需要添加部分 Babel 相关的包体来方便 Jest 对测试代码进行转化与编译:

$ npm install --save-dev babel-jest babel-preset-es2015 babel-preset-react

依赖安装完毕之后,我们就像配置其他的基于 Babel 的项目一样,需要添加 .babelrc 配置文件:

{
  "presets": ["es2015", "react"]
}

环境搭建完毕之后,我们就可以进行简单的测试用例编写了,譬如我们的代码库 sum.js 文件中有如下简单的相加函数:

export default function sum(a, b) {
  return a + b;
}

该函数对应的测试用例放置在 sum.test.js 文件中,我们可以参考 Maven 中的文件目录格式,尽量保持 src 与 test 目录下文件结构的一致性。Jest 也会为我们自动寻找项目目录下的以 .spec、.test 结尾的文件,或者放置在 __test__ 目录下的文件。我们的测试用例编写如下:

import sum from "../../src/util/sum.js";

test("adds 1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toBe(3);
});

测试代码编写完毕之后,我们使用 jest test/util/sum.test.js 命令来运行测试用例,可以在命令行中得到如下的反馈:

 PASStest/util/sum.test.js
  ✓ adds 1 + 2 to equal 3 (2ms)


Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time:0.565s, estimated 1s
Ran all test suites matching "test/util/sum.test.js".

到这里我们已经搭建了能够支撑 JavaScript 代码测试的环境,不过往往组件开发中,特别是基于 Webpack 等打包工具开发时,我们会在组件中导入 CSS、图片等静态资源。我们需要配置额外的 Mock 文件来处理这些静态资源,在 package.json 文件中添加以 jest 为键名的配置:

// package.json
{
  "jest": {
    "moduleNameMapper": {
      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
      "\\.(scss|css|less)$": "<rootDir>/__mocks__/styleMock.js"
    }
  }
}

其对应的 Mock 文件如下:

// __mocks__/styleMock.js
module.exports = {};

// __mocks__/fileMock.js
module.exports = "test-file-stub";

除了对于静态资源文件的配置之外,我们还可以使用类似于 Webpack 中的 modulesDirectoriesextensions 等配置项来自定义 Jest 的文件搜索规则:

// package.json
{
  "jest": {
    "modulePaths": ["/shared/vendor/modules"],
    "moduleFileExtensions": ["js", "jsx"],
    "moduleDirectories": ["node_modules", "bower_components", "shared"],
    "moduleNameMapper": {
      "\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js",
      "\\.(gif|ttf|eot|svg)$": "<rootDir>/__mocks__/fileMock.js"
    }
  }
}

Webpack

最后,我们还需要考虑如何在 Webpack 2 项目中进行 Jest 配置,其主要关注点在于 Webpack 2 提供了对于 ES2015 Modules 的原生支持;而 Jest 运行于 Node 环境下,仍然需要将 ES2015 Modules 转化为 CommonJS 模块规范。因此我们需要为 Webpack 2 项目下的 .babelrc 文件添加 test 环境的配置:

// .babelrc
{
  "presets": [["es2015", { "modules": false }]],

  "env": {
    "test": {
      "plugins": ["transform-es2015-modules-commonjs"]
    }
  }
}

而如果在组件开发中我们使用了动态模块导入,即 import('some-file.js').then(module => …) 这样的语法,我们还需要添加 dynamic-import-node 插件:

// .babelrc
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": ["syntax-dynamic-import"],
  "env": {
    "test": {
      "plugins": ["dynamic-import-node"]
    }
  }
}

TypeScript

在 Jest 中支持 TypeScript,我们首先需要添加相关的依赖:

$ yarn add -D typescript jest ts-jest @types/jest

我们可以通过自定义 preprocessor 来进行 TypeScript 处理:

// package.json
{
  "jest": {
    "moduleFileExtensions": ["ts", "tsx", "js"],
    "transform": {
      "^.+\\.(ts|tsx)$": "<rootDir>/preprocessor.js"
    },
    "testMatch": ["**/__tests__/*.(ts|tsx|js)"]
  }
}
const tsc = require("typescript");

const tsConfig = require("./tsconfig.json");

module.exports = {
  process(src, path) {
    if (path.endsWith(".ts") || path.endsWith(".tsx")) {
      return tsc.transpile(src, tsConfig.compilerOptions, path, []);
    }

    return src;
  }
};

或者直接使用 ts-jest:

module.exports = {
  transform: {
    "^.+\\.tsx?$": "ts-jest"
  },
  testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
  moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"]
};