起步
测试到底测什么
提到测试的时候,即使是最简单的一个代码块可能都让初学者不知所措。最常问的问题的是“我怎么知道要测试什么?”。如果你正在写一个 Web 应用,那么依次测试每个页面的用户交互方式,就是一个很好的开端了。
但 Web 应用也是由很多个函数和模块组成的代码单元,也是需要测试的。通常有两种情况:
- 你接手的遗留代码没有写测试用例
- 你必须从无到有的实现一个新功能
对于上面两种场景,你可以把测试视为代码的一部分来编写。我所说的这些代码,是用来检查给定的函数是否产生预期输出结果的。 一个典型的测试流程如下:
- 引入要测试的函数
- 给函数一个输入
- 定义预期输出
- 检查函数是否返回了预期的输出结果
即:输入 —— 预期输出 —— 验证结果。
示例
下面来看一个例子:
// math.js
function sum (a, b) {
return a + b
}
function subtract (x, y) {
return x - y
}
module.exports = {
sum,
subtract
}
如何保证上面代码的正确性?
下面来写一段测试代码:
注意:通常将实际代码与测试文件隔离放置,方便管理维护。
// math.test.js
const { sum, subtract } = require('./math')
// 测试示例1
// 运行结果
const result = sum(1, 2)
// 期望结果
const expected = 3
if (result !== expected) {
throw new Error(`1 + 2 应该等于 ${expected},但是结果却是 ${result}`)
}
// 测试示例2
// 运行结果
const result2 = subtract(2, 1)
// 期望结果
const expected2 = 1
if (result2 !== expected2) {
throw new Error(`2 - 1 应该等于 ${expected2},但是结果却是 ${result2}`)
}
通过测试代码可以很方便的帮助验证代码的正确性。
封装测试工具函数
之前示例的测试代码太过繁琐,可以思考一下能否封装的更简便一些,比如下面这样:
expect(sum(1, 2)).toBe(3)
expect(subtract(2, 1)).toBe(-1)
上面的测试代码就像自然语言说话一样,很舒服。
expect
称为断言函数:断定一个真实的结果是期望的结果。很多测试框架都有这个方法。
实现 expect 方法:
expect(sum(1, 2)).toBe(3)
expect(subtract(2, 1)).toBe(1)
function expect(result) {
return {
toBe(actual) {
if (result !== actual) {
throw new Error(`预期值和实际值不相等,预期 ${result},结果确实 ${actual}`)
}
}
}
}
增加错误提示信息:
// math.test.js
const { sum, subtract } = require('./math')
test('测试加法', () => {
expect(sum(1, 2)).toBe(3)
})
test('测试减法', () => {
expect(subtract(2, 1)).toBe(1)
})
function test(description, callback) {
try {
callback()
console.log(`${description} 通过测试`)
} catch (err) {
console.error(`${description} 没有通过测试:${err}`)
}
}
function expect(result) {
return {
toBe(actual) {
if (result !== actual) {
throw new Error(`预期值和实际值不相等,预期 ${result},结果确实 ${actual}`)
}
}
}
}
Jest 介绍
Jest 是 Facebook 出品的一个 JavaScript 开源测试框架。相对其他测试框架,其一大特点就是就是内置了常用的测试工具,比如零配置、自带断言、测试覆盖率工具等功能,实现了开箱即用。
Jest 适用但不局限于使用以下技术的项目:Babel,、TypeScript、 Node、 React、Angular、Vue 等。
Jest 主要特点:
- 零配置
- 自带断言
- 而作为一个面向前端的测试框架, Jest 可以利用其特有的快照测试功能,通过比对 UI 代码生成的快照文件,实现对 React 等常见前端框架的自动测试。
- 此外, Jest 的测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升了测试速度。
- 测试覆盖率
- Mock 模拟
快速体验 Jest
安装 Jest 到项目中:
npm init -y
npm install -D jest
package.json 添加脚本:
"scripts": {
"test": "jest"
},
编写实际代码:
// math.js
function sum(a, b) {
return a + b
}
function subtract(x, y) {
return x - y
}
module.exports = {
sum,
subtract
}
编写测试用例:
// math.test.js
const { sum, subtract } = require('./math')
test('测试加法', () => {
expect(sum(1, 2)).toBe(3)
})
test('测试减法', () => {
expect(subtract(2, 1)).toBe(1)
})
npm run test
运行测试命令。
- Jest 找到项目中所有以
.test.js
结尾的文件并运行 - Jest 会给测试文件注入
test
、expect
等全局函数,所以在测试文件中可以直接使用 - Jest 为测试结果提供了良好的日志输出
vscode 中 jest 代码智能提示
由于文件中并没有引入 Jest 的方法,所以使用的时候 vscode 没有提供智能提示。
可以通过安装 jest 的类型声明文件 @types/jest
来解决。
npm i -D @types/jest
注意:@types/jest
必须安装到项目的根目录,并且以根目录的方式在 vscode 中打开,否则不生效。
或者说只要是 vscode 打开的项目根目录有 @types/jest
这个包就可以了。
这是因为 TS 是从项目根目录下的 node_modules 查找 @types 类型声明文件的。
Jest 配置
配置文件
Jest 默认提供了零配置的使用方式。如果要修改默认配置规则,可以生成并修改配置文件。
# 生成 jest 配置文件
npx jest --init
# 配置文件的格式 ts or js
√ Would you like to use Typescript for the configuration file? ... no
# 测试环境 node 环境 或 jsdom 浏览器环境
√ Choose the test environment that will be used for testing » jsdom (browser-like)
# 是否需要 Jest 收集测试覆盖率报告
√ Do you want Jest to add coverage reports? ... no
# 用于统计测试覆盖率使用的引擎
# 目前最稳定是的 babel,v8 仍处于实验阶段,建议 node v14 版本以上使用
√ Which provider should be used to instrument code for coverage? » babel
# 是否在每次测试之前清除 mock 调用和相关实例
√ Automatically clear mock calls, instances and results before every test? ... yes
详细配置信息参考:配置 Jest(中文文档翻译不全,仅供参考)。
生成的配置文件 jest.config.js
中列出了一些配置选项,如果在生成时选择了非默认设置,就会取消注释覆盖默认配置,完整配置项请参考文档。
简单介绍几个:
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
// 自动 mock 所有导入的外部模块
// automock: false,
// Stop running tests after `n` failures
// 在指定次数失败后停止运行测试
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "C:\\Users\\Administrator\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls, instances and results before every test
// 在每个测试之间自动清除 mock 调用和实例
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// 是否收集测试覆盖率信息
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// 一个 glob 模式数组,指示应该为其收集覆盖率信息的一组文件
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// 测试覆盖率报错文件输出的目录
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// 忽略测试覆盖率统计的文件
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// Indicates which provider should be used to instrument code for coverage
// 指示应该使用哪个引擎检测代码的覆盖率,默认是 babel,可选 v8,但是 v8 不太稳定,建议 Node 14 以上版本使用
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// 运行 Jest 时运行的测试文件
// testMatch: [
// 所有 __tests__ 目录下的 .js .ts .jsx .tsx 文件
// "**/__tests__/**/*.[jt]s?(x)",
// 所有文件名带 .test. 或 .spec. 的 .js .ts .jsx .tsx 文件
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// 忽略测试的目录
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\",
// "\\.pnp\\.[^\\\\]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
Jest CLI
Jest 除了通过配置文件进行配置,还可以通过命令行参数进行配置。
参考:Jest CLI 选项 · Jest (jestjs.io)
有些配置只能在配置文件中使用,有些配置只能在命令行参数中使用,例如监视文件只用使用命令行参数 --watchAll
。
Jest 监视模式
–watchAll
监视文件的更改并在任何更改时重新运行所有测试。
jest --watchALl
–watch
该模式需要 Git 支持。
git init
初始化仓库
监视 Git 仓库中更改的文件,并重新运行与已更改的文件相关的测试。
jest --watch
监视模式中的辅助命令
使用监视模式后,命令行中会显示辅助命令提示:
Watch Usage
# 按 a 进入 a 模式:运行所有的测试。
# a 进入,a 退出
# 也可以使用 jest --watchAll 直接进入 a 模式
# 只有 jest --watch 时才能使用
› Press a to run all tests.
# 按 f 进入 f 模式:只运行失败的测试。
# f 进入,f 退出
› Press f to run only failed tests.
# 按 o 进入 o 模式:只运行与更改文件相关的测试。
# 需要 Git 支持
# 也可以使用 jest --watch 直接进入 o 模式
# 只有 jest --watchAll 时才能使用
› Press o to only run tests related to changed files.
# 按 p 以文件名正则表达式模式进行过滤。
# 只有 --watchAll 的时候 p 模式才可以使用
# 注意:testRegex 将尝试使用绝对文件路径来检测测试文件,因此,具有名称与之匹配的文件夹将所有文件作为测试运行
# testRegex 会忽略 testMatch
› Press p to filter by a filename regex pattern.
# 按 t 以测试名称(test 方法第一个参数)正则表达式模式进行过滤。
› Press t to filter by a test name regex pattern.
# 按 q 退出监视模式
› Press q to quit watch mode.
# 按 Enter 键触发测试运行
› Press Enter to trigger a test run.
使用 ES6 模块
如果要在 Jest 测试中使用 ES6 模块,则需要使用-babel:
# 安装 babel 相关依赖
npm i -D babel-jest @babel/core @babel/preset-env
// babel.config.js
module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
Jest 在运行测试的时候会自动找到 Babel 将 ES6 代码转换为 ES5 执行。
Jest 结合 Babel 的运行原理:运行测试之前,结合 Babel,先把代码做一次转化,模块被转换为了 CommonJS,运行转换之后的测试用例代码。