学校网站模板大全,北京 做网站,wordpress个人博客模板下载,免费的课程设计哪个网站有单元测试工具JEST入门——纯函数的测试 什么是测试❓#x1f649; 我只是开发而已#xff1f;常见单元测试工具 #x1f527;jest的使用#x1f440; 首先你得知道一个简单的例子#x1f330;#x1f628; Oops#xff01;出现了一些问题#x1f44f; 高效的持续监听 我只是开发而已常见单元测试工具 jest的使用 首先你得知道一个简单的例子 Oops出现了一些问题 高效的持续监听生成测试覆盖率报告 Jest 中的 mock Jest 中的钩子函数 常用的断言方法Mock Timer什么是快照测试 前端单元测试策略与原则⭕ 你可能会遇到的问题Demo 什么是测试❓
对于前端来说测试主要是对HTML、CSS、JavaScript进行测试以确保代码的正常运行。
常见的测试单元测试、集成测试、端到端的测试e2e
单元测试对程序中最小可测试单元进行测试。——零件测试集成测试对多个可执行单元组成的整体进行测试。——零件组装成的“发动机”测试端到端的测试从服务端到客户端的测试是对整体系统进行测试。——执行完整流程。
测试方式人工测试、自动测试
人工测试测试同学根据业务流程进行操作人工检查哪一步会出现问题。它只能测试出看得见的问题对于看不见的部分例如内部函数、逻辑代码等都有可能会出现问题。自动测试利用写好的代码对代码进行测试。能够弥补人工测试的不足它的颗粒度是代码级别的能够准确识别某个方法的错误。 在实际的开发过程中一般采用人工测试自动测试的方式力求100%覆盖测试目标。 我只是开发而已
bug发现在开发阶段成本很低如果发现在生产环境成本很高。
➢ 技术的角度有效的提高代码的健壮性有效的增加代码的可维护性对于后期的代码重构是必要条件。 ➢ 团队的角度可以有效的减少测试成本维护成本。 单元测试是前期慢后期快的工作做好单元测试可以大大缩短后期的测试和改 bug 的时间。 单元测试的好处
更少的调试经过测试的代码在提交时有较少的缺陷增加对变化的信心可以自信地审查和接受项目的变化更简单的审查CR中验证代码是否符合预期的精力就会减少深思熟虑的设计编写测试是锻炼代码本身的一种实用手段快速、高质量地发布可以放心地发布新版本的应用程序
❌ 因为单元测试有这些好处所以要做单元测试
✅ 如果不做单元测试会有什么样的问题
瀑布流开发项目的每个阶段都会带来种种需求不匹配的情况导致交付的最终价值可能不是客户所需要的。
敏捷开发迭代式的软件开发流程在每一个小的迭代周期内走过完整的流程并及时发布一个可用版本给用户风险提前暴露用户及时反馈快速进入下一个迭代降低需求不匹配带来的风险。
敏捷是为了更快交付有价值的、可工作的软件。 市场变化多端我们需要及时推出产品从而验证产品在市场上的用户反馈。 想法付诸行动 → 业务取得成效决策、设计、开发、测试、发布、反馈… …
戴明环是全面质量管理的思想基础和方法将质量管理分为四个阶段即Plan计划、Do执行、Check检查和 Action处理。
不断缩短反馈周期提高反馈速度才能减少不必要的浪费。
Q“敏捷交付最重要的是什么” A“快。快速迭代、持续交付用户价值。” Q“对我们开发者有什么影响” A“如果我们不写单元测试我们就快不起来。”
每次开发上线团队都要投入人力来进行手工测试其中也包括你自己 因为没有测试不敢随意重构从而导致代码逐渐腐化随着项目的进展代码越来越复杂复杂的代码会导致开发速度降低从而陷入死循环。代码越来越烂、开发越来越慢、BUG越来越多。 除此之外我们整个项目的人员会流动应用会变得越来越复杂功能会变得越来越多那么人员一定会流动需求一定会增加直到整个项目没有一个人可以了解到应用的所有功能那么对软件进行修改的成本就会非常高。 如果试图依赖人工方式来应对快速变化的市场挑战是非常高的。而单元测试是自动化的能够给我们极快的反馈速度。
所以单元测试是非常有必要的
常见单元测试工具
目前用的最多的前端单元测试框架主要有 Mocha、Jest但推荐使用 Jest因为 Jest 和 Mocha 相比无论从 github stats issues 量npm下载量相比都有明显优势。 详见github stats 以及 npm 下载量的实时数据
jest的使用
Jest 是 Facebook 开发的一款 JavaScript 测试框架在 Facebook 内部广泛用来测试各种 JavaScript 代码。
轻松上手高性能支持测试的并发运行内置强大的断言与 mock 功能内置测试覆盖率统计功能内置 Snapshot快照机制
npm install --save-dev jest首先你得知道
Jest 的测试脚本名形如*.test.js不论 Jest 是全局运行还是通过 npm test 运行它都会执行当前目录下所有的*.test.js 或 *.spec.js 文件完成测试
test(name, fn, timeout)是将运行测试的方法。也叫 it(name, fn, timeout)describe(name, fn)是一个将多个相关的测试组合在一起的块。
// add.js
function add(a, b) {return a b
}// add.test.js
describe(add function, () {test(adds 1 2 to equal 3, () {const result add(1, 2)expect(result).toBe(3)})it(adds 5 7 to equal 12, () {const result add(5, 7)expect(result).toBe(12)})
})一个简单的例子
// sum.js
function sum(a, b) {return a b
}
module.exports sum// sum.test.js
import { expect, test } from jest/globals
const sum require(./sum)test(adds 1 2 to equal 3, () {expect(sum(1, 2)).toBe(3)
})// package.json
{scripts: {test: jest},
}运行yarn test 或者 npm test Oops出现了一些问题
当我想用 import 来引入时出现了这样的问题
SyntaxError: Cannot use import statement outside a module
原因nodejs 采用的是 CommonJS 的模块化规范使用 require 引入模块而 import 是 ES6 的模块化规范关键字。想要使用 import必须引入 babel 转义支持通过 babel 进行编译使其变成 node 的模块化代码
npm install --save-dev babel/core babel/preset-env项目的根目录 .babelrc.js
module.exports {presets: [babel/preset-env]
}问题解决 原因jest 运行时内部先执行( jest-babel )检测是否安装 babel-core然后取 .babelrc 中的配置运行测试之前结合 babel 先把测试用例代码转换一遍然后再进行测试
如果我想测试ts jest只能测试 js 文件 要对其他类型的文件进行测试则需要使用其他的扩展在typescript项目中我们可以使用babel或者ts-jest来实现项目对ts测试的支持。
第一种使用 babel
// 安装依赖
npm install --save-dev babel/preset-typescript// 项目的根目录 .babelrc.js
presets: [babel/preset-typescript, babel/preset-env]第二种使用 ts-jest
// 安装依赖
npm add --save-dev jest ts-jest types/jest区别在对 Typescript 的测试中因为Babel对 Typescrip 的支持是纯编译形式无类型校验所以babel/preset-typescript 并不能 ts 类型进行检查所以如果需要类型校验你就需要使用 ts-jest 来进行 Typescrip的支持。 详见使用Typescript 高效的持续监听
为了提高效率可以通过加启动参数的方式让 jest 持续监听文件的修改而不需要每次修改完再重新执行测试用例在package.json中
test: jest --watchAll生成测试覆盖率报告 什么是单元测试覆盖率 指在所有功能代码中完成了单元测试的代码所占的比例。 单元测试覆盖率 被测代码行数 / 参测代码总行数 * 100% 两种方法
命令执行
npm test --coverage在 jest.config.js 中配置
module.exports {// 是否显示覆盖率报告collectCoverage: true,// 告诉 jest 哪些文件需要经过单元测试测试collectCoverageFrom: [src/utils/**/*],
}这里我以utils下的所有文件为例
参数名含义说明% Stmts语句覆盖率是否每条语句都执行了% Branch分支覆盖率是否每个情况分支都执行了例如 if-else% Funcs函数覆盖率是否每个函数都调用了% Lines行覆盖率是否每一行都执行了Uncobered Line未覆盖到的行未覆盖到的行数
Jest 中的 mock
为什么Jest需要模拟函数
世界软件开发大师Martin Fowler根据是否依赖其他模块将单元测试分为了社交型测试单元和独立型测试单元。
在测试中我们特别需要注意不同模块之间的依赖
Database数据库Network Requests网络请求Access to Files存取文件Any External System任何外部系统
这些依赖我们需要“扮演者”也就是模拟函数
这里我们主要了解Jest 中的三个与 Mock 函数相关的API分别是 jest.fn()、jest.mock()、jest.spyOn()。
模拟函数 jest.fn() jest.fn(implementation) 是创建Mock函数最简单的方式用于模拟特定行为。 我们可以设置该函数的返回值、监听该函数的调用、改变函数的内部实现等等我们通过 jest.fn() 创建的函数有一个特殊的 .mock 属性该属性保存了每一次调用情况。
写一个单元测试
export const myMap (arr, fn) {return arr.map(fn)
}如代码中的单元测试所示只需要判断一下函数的返回结果即可。
import { myMap } from ./myMap
it(测试 map方法, () {// 自定义方法const fn (item) item * 2expect(myMap([1, 2, 3], fn)).toEqual([2, 4, 6])
})问题那如果我需要更细致地去判断每一次调用传进去的函数是否是数组中的每一项或者函数是否被调用了三次那该怎么写单元测试
import { myMap } from ./myMap
it(测试 map 方法, () {// 通过jest.fn声明的函数可以被追溯const fn jest.fn((item) (item * 2))expect(myMap([1, 2, 3], fn)).toEqual([2, 4, 6])// 调用3次expect(fn.mock.calls.length).toBe(3)// 每次函数返回的值是 2,4,6expect(fn.mock.results.map((item) item.value)).toEqual([2, 4, 6])// 打印 fn.mockconsole.log(fn.mock)
})打印结果
如果没有定义函数内部的实现jest.fn() 会返回 undefined 作为返回值。
it(测试返回 undefined, () {const myFn jest.fn()const result myFn({ a: 1 })// undefinedconsole.log(result)
})还可以设置返回值定义内部实现或返回Promise对象。
describe(test, () {it(测试设置返回值, () {const myFn jest.fn().mockReturnValue(fffff)expect(myFn()).toBe(fffff)})it(测试定义内部实现, () {const myFn jest.fn((a, b) a * b)expect(myFn(10, 20)).toBe(200)})it(测试返回Promise对象, async () {const asyncMock jest.fn().mockResolvedValue(default)const result await asyncMock()expect(Object.prototype.toString.call(asyncMock())).toBe([object Promise])})
})模拟文件jest.mock() jest.mock(moduleName, factory, options)用来mock一些模块或者文件 // service.js
import { getNames } from ../common/service
export const searchNames async (keyword) {// 获取接口数据const namesList await getNames()return namesList.filter((item) item.includes(keyword))
}// service.test.js
jest.mock(../common/service, () ({getNames: jest.fn(() [Jack, Rose])
}))test(find target result, () {const keyword Jackconst result searchNames(keyword)expect(result).toEqual([Jack])
})jest.spyOn() jest.spyOn(object, methodName)用来创建一个被监视spied的函数返回一个mock function和 jest.fn 相似但是能够追踪object[methodName]的调用信息。 // multiply.js
export const math {multiply: (a, b) {return a b},
}// multiply.test.js
import { math } from ./multiplyit(should spy on add function, () {// 创建一个被监视的函数const spy jest.spyOn(math, multiply)// 执行测试逻辑const result math.multiply(2, 3)// 验证函数是否被调用expect(spy).toHaveBeenCalled()// 验证函数的返回值expect(result).toBe(5)// 清除监视器spy.mockRestore()
})Jest 中的钩子函数
beforeAll(fn, timeout)所有测试之前执行。 设置一些在测试用例之间共享的全局状态。afterAll(fn, timeout)所有测试执行完之后。 清理一些在测试用例之间共享的全局状态。beforeEach(fn, timeout)每个测试实例之前执行。 重新设置一些全局状态在每个测试开始前。afterEach(fn, timeout)每个测试实例完成之后执行。 清理一些在每个测试中创建的临时状态。
如果传入的回调函数返回值是 promise 或者 generatorJest 会等待 promise resolve 再继续执行。 可选地传入第二个参数 timeout毫秒 指定函数执行超时时间。 The default timeout is 5 seconds。
// counter.ts
let counter 0
export function increment() {counter
}
export function decrement() {counter--
}
export function getCounter() {return counter
}// counter.test.ts
describe(Counter functions, () {// beforeAll: 在所有测试用例运行之前执行一次beforeAll(() {console.log(Before all tests)})// afterAll: 在所有测试用例运行之后执行一次afterAll(() {console.log(After all tests)})// beforeEach: 在每个测试用例运行之前执行beforeEach(() {console.log(Before each test)increment() // 在每个测试用例前对计数器进行递增})// afterEach: 在每个测试用例运行之后执行afterEach(() {console.log(After each test)decrement() // 在每个测试用例后对计数器进行递减})it(increments the counter, () {console.log(getCounter())})it(decrements the counter, () {console.log(getCounter())})
})常用的断言方法
仅列举常用方法更多内容详见Jest 官网 API
.not 修饰符允许你测试结果不等于某个值的情况 .toHaveLength 可以很方便的用来测试字符串和数组类型的长度是否满足预期 .toThorw 能够让我们测试被测试方法是否按照预期抛出异常 .toMatch 传入一个正则表达式它允许我们来进行字符串类型的正则匹配 .toContain 匹配对象中是否包含
检查一些特殊的值nullundefined 和 boolean toBeNull 仅匹配 null toBeUndefined 仅匹配 undefined toBeDefined 与…相反 toBeUndefined toBeTruthy 匹配 if 语句视为 true 的任何内容 toBeFalsy 匹配 if 语句视为 false 的任何内容
检查数字类型number toBeGreaterThan 大于 toBeGreaterThanOrEqual 大于等于 toBeLessThan 小于 toBeLessThanOrEqual 小于等于 toBeCloseTo 用来匹配浮点数带小数点的相等
Mock Timer
假如现在有一个函数 afterTime它的作用是在 2000ms 后执行传入的 callback
export const afterTime (callback) {console.log(准备计时)setTimeout(() {console.log(时间到了)callback callback()}, 2000)
}如果不 Mock 时间那么我们就得写这样的用例
test(wait time, (callback) {afterTime(() {callback()expect(undefined)})
})这样我们得死等 2000 毫秒才能跑这完这个用例这非常不合理。
先用 jest.useFakeTimers Mock 定时器并监听 setTimeout。mock一个 callback 函数执行 afterTime 后 对 callback 的调用做了一些断言。
describe(afterTime, () {beforeAll(() {// mock定时器jest.useFakeTimers()})test(fast, () {// 监听setTimeoutjest.spyOn(global, setTimeout)// mock一个函数const callback jest.fn()// 执行afterTimeafterTime(callback)// 此时断言这个函数没有被调用expect(callback).not.toHaveBeenCalled()// 快进时间jest.runAllTimers()// 断言这个函数被调用了expect(callback).toHaveBeenCalled()})
})什么是快照测试
直接上代码
// searchName.ts
export const sum (a, b) a b// searchName.test.js
test(sum, () {expect(sum(1, 3)).toMatchInlineSnapshot()
})运行npm test后发现代码中自动出现了 expect(sum(1, 3)).toMatchInlineSnapshot(4)
但是当我们随意更改sum方法的参数时你会发现报错了 它需要我们执行npm test -- -u来更新它。
明明代码没有问题只是修改了传参测试却出错了这就是测试中的“假错误”。虽然普通的单测中也有可能会出现“假错误”但是快照测试中出现的概率更高这也是很多人不信任快照测试的主要原因。
当我们改成使用toMatchSnapshot方法后发现当前文件夹下出现了.snap文件打开可以看到toMatchSnapshot将结果放在了这个文件里。 对于那种输出很复杂而且不方便用 expect 做断言时快照测试是一个好方法 快照测试的思想先执行一次测试把输出结果记录到 .snap 文件以后每次测试都会把输出结果和 .snap 文件做对比。
快照失败有两种可能
业务代码有问题要排查 Bug新增功能改变了原有结构要用 npx jest --updateSnapshot 更新当前快照假错误
现实中更多的情况是既在重构又要加新需求如果开发者滥用快照测试并生成很多大快照 那么最终的结果是没有人再相信快照测试。一遇到快照测试不通过都不愿意探究失败的原因而是选择更新快照来 “糊弄一下”。
要避免这样的情况需要做好两点
生成小快照。 只取重要的部分来生成快照必须保证快照是能让你看懂的合理使用快照。
前端单元测试策略与原则 “你的测试与你的软件使用方式越相似它们就越能给你带来信心。” 模块依赖和调用时的方法都应该像软件模块真正被使用的时候一样。
思考是否可以让测试来驱动开发 先写测试再写业务代码当所有测试用例都通过后你的业务代码也就实现完成了。
先写一个期望测试得到“失败” ✅开发代码使测试通过 进行重构使其可维护性更高
适用场景
纯函数。不管逻辑是否简单我们都很容易想到输入与输出那么我们可以先写测试用例覆盖90%的场景。UI交互。Mock需要的HTTP请求用测试模拟用户操作再去实现业务逻辑。修BUG。用一个case来模拟复现问题进行修复。
其实我们平常打的log不仅log出来后需要删掉而且最最重要的是只能测试一两种case我们还需要手动刷新页面进行肉眼找茬。从执行到肉眼看结果这就是一种手动测试。
// 用测试用例来描述这个需求
import objToSearchStr from utils/objToSearchStr
describe(objToSearchStr, () {test(可以将对象转化成查询参数字符串, () {expect(objToSearchStr({ a: 1, b: 2 })).toEqual(a1b2)})
})// 边看业务输入输出边实现代码逻辑
const objToSearchStr (obj: Recordstring, string | number) {// [a1, b2]const strPairs: string[] []Object.entries(obj).forEach((keyValue) {const [key, value] keyValue // [a, 1]const pair key String(value) // a1strPairs.push(pair)}, [])// a1b2return strPairs.join()
}
export default objToSearchStr你的代码的易测性也就代表着代码的可维护性。 反思在保证单元测试独立性的前提下什么样的模块才是符合【职责单一】原则的
React 测试库 官网React Testing Library 下载npm install --save-dev testing-library/react 文档引入 React纯配置
⭕ 你可能会遇到的问题
使用 jest.mock() 时文件路径找不到
✅ 解决在 jest.config.js 文件中配置路径中配置模块路径的别名
{jest: {moduleNameMapper: {^/(.*)$: rootDir/src/$1}}
}在使用 jest.spyOn() 时报错Property xxx does not exist in the provided object 原因jest.spyOn() 用于监视对象的属性或方法但在示例中 multiply 并不是一个对象而是一个直接导出的函数。
✅ 解决将 multiply 放置在一个对象上然后导出这个对象
// 错误代码
export const multiply (a, b) {return a b
}// 改为
export const math {multiply: (a, b) {return a b},
}测试异步函数时报错 regeneratorRuntime is not defined 原因这是因为 babel/preset-env 不支持 async await 导致的。
✅ 解决要对 babel 配置进行增强可以安装 babel/plugin-transform-runtime 这个插件解决
// 安装依赖
npm install --save-dev babel/plugin-transform-runtime// .babelrc.js 配置
plugins: [babel/plugin-transform-runtime]Demo
// searchName.ts
import { getNames } from ../common/service;export const searchNames (keyword) {const results getNames().filter((item) item.includes(keyword));return results.length 3 ? results.slice(0, 3) : results;
};export const sum (a, b) a bexport const afterTime (callback) {console.log(准备计时)setTimeout(() {console.log(时间到了)callback callback()}, 2000)
}// searchName.test.js
import { searchNames, sum, afterTime } from ./searchName
import { getNames } from ../common/servicejest.mock(../common/service, () ({getNames: jest.fn()
}))test(sum, () {expect(sum(1, 3)).toMatchInlineSnapshot(4)
})// mockImplementation接受应该用作模拟实现的函数当调用模拟时实现也会被执行test(should return search empty result, () {const keyword RosegetNames.mockImplementation(() [Jack])const result searchNames(keyword)expect(result).toEqual([])
})test(find target result, () {const keyword JackgetNames.mockImplementation(() [Jack, Rose])const result searchNames(keyword)expect(result).toEqual([Jack])
})test(not return more than 3 result, () {const keyword JackgetNames.mockImplementation(() [Jack1, Jack2, Jack3, Jack4])const result searchNames(keyword)expect(result).toHaveLength(3)
})test(should handle null or undefined as input, () {expect(searchNames(null)).toEqual([])expect(searchNames(undefined)).toEqual([])
})describe(afterTime, () {test(wait time, (callback) {afterTime(() {callback()expect(undefined)})})beforeAll(() {// mock定时器jest.useFakeTimers()})test(fast, () {// 监听setTimeoutjest.spyOn(global, setTimeout)// mock一个函数const callback jest.fn()afterTime(callback)// 断言这个函数没有被调用expect(callback).not.toHaveBeenCalled()// 快进时间jest.runAllTimers()// 断言这个函数被调用了expect(callback).toHaveBeenCalled()expect(setTimeout).toHaveBeenCalledTimes(1)})
})