前言
在 工欲善其事必先利其器(配置Vue3 + ts项目模板) 篇中还有一些内容需要完善,比如测试相关的内容,本篇就在该篇模板中集成测试相关的内容
为什么需要测试
测试在前端项目中的重要性主要体现在以下几个方面:
1. 质量保证:测试可以帮助开发者发现和修复代码中的错误和漏洞,从而提高产品的质量和稳定性
2. 性能优化:通过性能测试,开发者可以找出代码中的性能瓶颈,从而进行优化,提高用户体验
3. 可维护性:良好的测试覆盖率可以确保代码的可维护性,当开发者需要修改或添加新功能时,可以通过测试来确保现有功能的稳定性
4. 自动化:自动化测试可以大大提高开发效率,减少人工测试的时间和成本
5. 回归测试:当代码发生变化时,可以通过回归测试来确保现有功能不会被破坏
测试种类
前端测试是一种软件测试方法,主要关注在用户界面层面的功能、性能和用户体验。它包括以下几种类型:
1. 单元测试:测试单个组件或模块的功能。常用的工具有 Jest, Mocha, Jasmine 等
2. 集成测试:测试多个组件或模块如何协同工作。常用的工具有 Jest, Cypress 等
3. 端到端测试(E2E 测试):测试整个应用的工作流程。常用的工具有 Cypress, Puppeteer, Protractor 等
4. 性能测试:测试应用在高负载下的表现。常用的工具有 Lighthouse, YSlow 等
5. 可访问性测试:测试应用对残障人士的可用性。常用的工具有 aXe, PA11Y 等
6. 兼容性测试:测试应用在不同浏览器和设备上的表现
7. 安全性测试:测试应用的安全性,包括 XSS 攻击,CSRF 攻击等。常用的工具有 OWASP ZAP, Nessus 等
8. 视觉回归测试:测试应用的 UI 是否有变化。常用的工具有 Percy, BackstopJS 等
每个测试阶段都有其重要性,需要根据项目的具体需求来决定使用哪些测试和工具
一般来讲,前端开发人员需要关注的是单元测试、集成测试,其他的也可以深入了解,这样可以更好的跟上下游沟通
单元测试
测试相关的工具有很多,这里这里就不一一介绍了,这里就选择 Jest
Jest是一个流行的JavaScript测试框架,有以下几个优点:
1. 零配置:Jest默认提供了大部分你需要的配置,使得设置测试环境变得简单。
2. 快速并行测试:Jest可以并行运行测试,这使得测试速度更快。
3. 快照测试:Jest提供了快照测试功能,可以轻松地测试UI组件。
4. 覆盖率报告:Jest可以生成测试覆盖率报告,帮助你了解哪些代码已经被测试,哪些还没有。
5. Mock函数:Jest提供了强大的mock功能,可以轻松地模拟函数和模块。
6. 与主流框架兼容:Jest与React、Vue、Angular等主流框架兼容。
7. 社区支持:Jest有一个活跃的社区和丰富的文档,这使得解决问题变得更容易。
下面基于 vue3
项目集成 Jest
Jest 安装 & 配置
# 安装
pnpm i -D jest
# 交互式生成配置
pnpm jest --init
# 安装jsdom包,如果选择的不是 jsdom环境可以不装
pnpm i -D jest-environment-jsdom
生成的 jest.config.ts
(初始化选择了 typescript) 如下
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type {Config} from 'jest';
const config: Config = {
// 自动模拟测试中导入的所有模块
// automock: false,
// 在`n`个失败后停止运行测试
// bail: 0,
// Jest应存储其缓存依赖信息的目录
// cacheDirectory: "",
// 在每个测试之前自动清除模拟调用、实例、上下文和结果
clearMocks: true,
// 指示是否应在执行测试时收集覆盖信息
collectCoverage: true,
// 一个glob模式数组,指示应收集哪些文件的覆盖信息
// collectCoverageFrom: undefined,
// Jest应输出其覆盖文件的目录
coverageDirectory: "coverage",
// 用于跳过覆盖收集的正则表达式模式字符串数组
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// 指示应使用哪个提供者来为覆盖进行代码插桩
// coverageProvider: "babel",
// Jest在编写覆盖报告时使用的报告器名称列表
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// 一个配置覆盖结果最小阈值执行的对象
// coverageThreshold: undefined,
// 自定义依赖项提取器的路径
// dependencyExtractor: undefined,
// 使调用已弃用的API抛出有用的错误消息
// errorOnDeprecated: false,
// 伪造计时器的默认配置
// fakeTimers: {
// "enableGlobally": false
// },
// 使用glob模式数组从忽略的文件中强制收集覆盖
// forceCoverageMatch: [],
// 一个模块的路径,该模块导出一个在所有测试套件之前触发一次的异步函数
// globalSetup: undefined,
// 一个模块的路径,该模块导出一个在所有测试套件之后触发一次的异步函数
// globalTeardown: undefined,
// 需要在所有测试环境中可用的全局变量集
// globals: {},
// 用于运行测试的最大工作量。可以指定为%或数字。例如,maxWorkers: 10%将使用您的CPU数量的10%+1作为最大工作量。maxWorkers: 2将使用最多2个工作量。
// maxWorkers: "50%",
// 一个目录名数组,从需要的模块位置递归向上搜索
// moduleDirectories: [
// "node_modules"
// ],
// 模块使用的文件扩展名数组
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// 从正则表达式到模块名的映射,或者到允许使用单个模块来存根资源的模块名数组的映射
// moduleNameMapper: {},
// 一个正则表达式模式字符串数组,与所有模块路径匹配,匹配的模块在被模块加载器视为'可见'之前
// modulePathIgnorePatterns: [],
// 激活测试结果的通知
// notify: false,
// 指定通知模式的枚举。需要{ notify: true }
// notifyMode: "failure-change",
// 用作Jest配置基础的预设
// preset: undefined,
// 从一个或多个项目运行测试
// projects: undefined,
// 使用此配置选项向Jest添加自定义报告器
// reporters: undefined,
// 在每个测试之前自动重置模拟状态
// resetMocks: false,
// 在运行每个单独的测试之前重置模块注册表
// resetModules: false,
// 自定义解析器的路径
// resolver: undefined,
// 在每个测试之前自动恢复模拟状态和实现
// restoreMocks: false,
// Jest应扫描测试和模块的根目录
// rootDir: undefined,
// Jest应用于搜索文件的目录路径列表
// roots: [
// "<rootDir>"
// ],
// 允许您使用自定义运行器而不是Jest的默认测试运行器
// runner: "jest-runner",
// 运行一些代码以配置或设置测试环境的模块路径
// setupFiles: [],
// 运行一些代码以配置或设置测试框架的模块路径列表
// setupFilesAfterEnv: [],
// 考虑为慢速的测试,并在结果中报告为此的秒数。
// slowTestThreshold: 5,
// Jest应用于快照测试的快照序列化模块路径列表
// snapshotSerializers: [],
// 将用于测试的测试环境
testEnvironment: "jsdom",
// 将传递给测试环境的选项
// testEnvironmentOptions: {},
// 在测试结果中添加位置字段
// testLocationInResults: false,
// Jest用于检测测试文件的glob模式
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// 一个与所有测试路径匹配的正则表达式模式字符串数组,匹配的测试将被跳过
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// Jest用于检测测试文件的正则表达式模式或模式数组
// testRegex: [],
// 此选项允许使用自定义结果处理器
// testResultsProcessor: undefined,
// 此选项允许使用自定义测试运行器
// testRunner: "jest-circus/runner",
// 从正则表达式到转换器路径的映射
// transform: undefined,
// 一个与所有源文件路径匹配的正则表达式模式字符串数组,匹配的文件将跳过转换
// transformIgnorePatterns: [
// "\\\\node_modules\\\\",
// "\\.pnp\\.[^\\\\]+$"
// ],
// 一个与所有模块匹配的正则表达式模式字符串数组,模块加载器在自动为它们返回模拟之前,匹配的模块
// unmockedModulePathPatterns: undefined,
// 指示是否应在运行期间报告每个单独的测试
// verbose: undefined,
// 一个与所有源文件路径匹配的正则表达式模式数组,在观察模式下重新运行测试之前
// watchPathIgnorePatterns: [],
// 是否使用watchman进行文件爬取
// watchman: true,
};
export default config;
这个非常简单,详细请参考 jest文档 即可
完善配置
虽说 Jest
是“零”配置的,但是还需要根据实际应用场景进行相应配置
指定测试文件后缀
建议更改如下配置,指定带有 .spec
或.test
后缀的文件才执行测试
testMatch: ['**/__tests__/?(*.)+(spec|test).[tj]s?(x)'],
别名
moduleNameMapper
字段可配置路径别名,moduleFileExtensions
配置文件拓展支持
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
moduleFileExtensions: [
"js",
"jsx",
"ts",
"tsx",
"json",
"vue"
],
es6 语法支持
pnpm i -D babel-jest @babel/core @babel/preset-env
配置 .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": true
}
}
]
]
}
ts 支持
pnpm i -D @types/jest ts-jest
tsconfig.json
中 compilerOptions.types
字配置添加 jest
此时 ts-jest
不支持 import.mate
需要补充 ts-jest-mock-import-meta 插件
pnpm i -D ts-jest-mock-import-meta
jest.config.ts
的 transform
更改相应配置如下,其中metaObjectReplacement
可以配置需要的环境变量,参考 AST transformers option | ts-jest
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
diagnostics: {
ignoreCodes: [1343]
},
astTransformers: {
before: [
{
path: 'node_modules/ts-jest-mock-import-meta', // or, alternatively, 'ts-jest-mock-import-meta' directly, without node_modules.
options: { metaObjectReplacement: { env: { NODE: '' } } }
}
]
}
}
]
}
转换node_modules 中的文件
jest 默认是不转换 node_modules 文件夹的,一般 node_modules 下的库都是已经转换过的了,但是也有例外,比如 react-native,详细说明请参考 测试React Native程序,也可参考下文第三方ESM模块映射模块
测试
写一个简单的用例试试
在 src/utils/__tests__/is.spec.ts
文件写入以下测试内容
import { isString } from '@/utils/is'
describe('is function', () => {
it('”“ is string', () => {
expect(isString('')).toBe(true)
})
})
package.json
配置 scripts
"scripts": {
"test": "jest"
},
执行 pnpm test
, 结果如下
集成测试
根据项目具体情况先完善下配置
vue 支持
可以结合 Vue Test Utils 来配置
pnpm i -D @vue/test-utils @vue/vue3-jest
jest.config.ts
添加配置
transform: {
'^.+\\.jsx?$': 'babel-jest', //这个是jest的默认配置
'^.+\\.ts?$': 'ts-jest', //typescript转换
'^.+\\.vue?$': '@vue/vue3-jest'
},
// 当下(注意时效)最新版本需要配置,否则会有些问题
testEnvironmentOptions: {
customExportConditions: ['node', 'node-addons']
}
第三方ESM模块映射
如果是整个第三方模块都是 ESM 规范的可以配置 transformIgnorePatterns
,参考 测试React Native程序
"transformIgnorePatterns": [
"node_modules/(?!(react-native|my-project|react-native-button)/)"
]
如果是某些子模块单独处理的,可以配置 moduleNameMapper
映射,比如 ant-design-vue
的国际化模块
moduleNameMapper: {
'ant-design-vue/es/locale/(.*)': '<rootDir>/node_modules/ant-design-vue/lib/locale/$1'
}
图片等静态资源映射
同样可以配置 moduleNameMapper
映射或者配置 transform
进行转换(不要同时配置),可参考 使用 webpack · Jest 和 代码转换 · Jest
moduleNameMapper: {'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js'
},
// 或者配置transform
transform: {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileTransformer.js'
}
__mocks__/fileMock.js
module.exports = 'jest-file-stub'
__mocks__/fileTransformer.js
module.exports = {
process(sourceText, sourcePath, options) {
return {
code: `module.exports = "jest-file-transform"`
}
}
}
vue 依赖注入
可以参考以下方式
import { shallowMount } from '@vue/test-utils'
import { createApp } from 'vue'
import { createStore } from 'vuex'
import App from '@/components/connect'
const store = createStore({
state() {
return {
user: {},
}
},
mutations: {},
})
let wrapper
const app = createApp(App)
app.use(store)
beforeEach(() => {
wrapper = shallowMount(App, {
propsData: {},
global: {
plugins: [store],
stubs: {
'el-button': {
template: '<i />',
},
},
},
})
})
但是如果每个测试用例都这么写也很麻烦
也可以设置 setupFiles
启动时直接执行
setupFiles: ['./__mocks__/setupFiles.js'],
__mocks__/setupFiles.js
import { createApp } from 'vue'
import pinia from '@/store'
beforeAll(() => {
const app = createApp(App)
app.use(pinia)
})
module.exports = async () => {
global.xxx = 'xxx';
}
不过并不是所有测试用例都需要执行,这样设置也不好
抽离公共部分
我们可以把公共部分抽出来
src/utils/test-utils.ts
import type { Component } from 'vue'
import { shallowMount } from '@vue/test-utils'
import pinia from '@/store'
import * as Antd from 'ant-design-vue'
import * as Icons from '@ant-design/icons-vue'
import { setActivePinia } from 'pinia'
export function shallowMountComponent(App: Component, props = {}) {
setActivePinia(pinia)
const stubs = {}
for (const i in Icons) {
if (Object.prototype.hasOwnProperty.call(Icons, i)) {
const Icon = Icons[i]
if (Icon.name) {
stubs[Icon.name] = Icon
}
}
}
for (const key in Antd) {
if (Object.prototype.hasOwnProperty.call(Antd, key)) {
const AComponent = Antd[key]
if (AComponent.name && AComponent.name.startsWith('A')) {
stubs[AComponent.name] = AComponent
}
}
}
return shallowMount(
App,
Object.assign(
{
global: {
pinia: pinia,
stubs
}
},
props
)
)
}
mock
一些模块或者数据需要模拟
axios
详细可参考 Mocking Axios in Jest + Testing Async Functions | Leigh Halliday
这里简单说说,可以抽出一个模块,如下
const mockAxios = jest.genMockFromModule('axios');
mockAxios.create = jest.fn(() => mockAxios);
mockAxios.get = jest.fn(() => Promise.resolve({ data: {} }));
mockAxios.post = jest.fn(() => Promise.resolve({ data: {} }));
mockAxios.put = jest.fn(() => Promise.resolve({ data: {} }));
mockAxios.delete = jest.fn(() => Promise.resolve({ data: {} }));
mockAxios.all = jest.fn(() => Promise.resolve());
export default mockAxios;
在使用的时候再调整,比如
import mockAxios from '../__mocks__/axios'
mockAxios.post.mockResolvedValue(() => {
return Promise.resolve({
code: 0,
msg: 'ok',
data: {
access_token: 'access_token',
expires_in: 12345
}
})
}
)
也可以根据不同 url
返回不同数据
mockAxios.get.mockResolvedValue(url => {
switch (url) {
case "./getUserInfoUrl":
return {
code: 0,
msg: 'ok',
data: {
id: 11111,
userName: 'test'
}
}
default:
return response
}
})
vuex
vuex
可以设置 plugins
shallowMount(
App,
{
global: {
plugins: [store],
stubs: {}
}
}
)
pinia
和vuex
类似,详细可参考 Testing stores | Pinia (vuejs.org)
测试
SwitchLogin.spec.ts
写入测试用例
import mockAxios from '_/axios'
import { userInfoSession } from '@/utils'
import { shallowMountComponent } from '@/utils/test-utils'
import SwitchLogin from '@/components/common/SwitchLogin.vue'
describe('SwitchLogin vue compoment', () => {
beforeEach(() => {
mockAxios.post.mockResolvedValue(() => {
return Promise.resolve({
code: 0,
msg: 'ok',
data: {
access_token: 'access_token',
expires_in: 12345
}
})
})
// const user = useUserStore()
userInfoSession(JSON.stringify({ id: 0, userName: 'test' }))
// apiTokenSession({ access_token: 'test', expires_in: 10000000 })
})
test('测试登出', async () => {
const wrapper = shallowMountComponent(SwitchLogin)
expect(userInfoSession().userName).toContain('test')
await wrapper.vm.handleMenuClick({ key: 'logout' })
expect(userInfoSession().userName).toContain('init test')
})
})
关于使用的相关介绍就简单介绍到这里,当然除了这些还有 timer、异步测试、快照等等,这里就不一一介绍了,详细的请参考 Vue Test Utils 文档
测试覆盖率
接下来来看看覆盖率,这个也比较简单
测试覆盖率报告
执行jest
命令配置--coverage
指令参数即可,生成的报告文件位置对应 jest.config.ts
里 coverageDirectory
字段的配置
jest --coverage
集成进 git hooks
首先是配置 jest.config.ts
的 coverageThreshold
字段,对分支、行和函数覆盖率进行要求,比如
coverageThreshold: {
"global": {
"branches": 50,
"functions": 50,
"lines": 50,
"statements": 50
},
"./src/components/": {
"branches": 40,
"statements": 40
},
"./src/utils": {
"statements": 90
},
"./src/directive": {
"statements": 90
},
"./src/utils/is.ts": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
},
以上配置根据不同场景设置了不同的覆盖率,配置全局、局部模块、单个文件为等,当测试覆盖率没达到配置要求时,执行测试的最终结果为不通过,会使测试用例全部通过也会被判定为失败,有了这个配置就可以集成进 husky
的钩子中了
比如在pre-commit
钩子文件中添加 pnpm jest
命令,这样当测试覆盖率未达到要求或者有失败的测试用例都会阻止 commmit
行为 ,同时每次提交都会执行一次测试,防止有未通过测试要求的代码被提交
关于测试覆盖率就简单说到这里,当然前端项目不要盲目堆测试覆盖率,对于公共且不常变更的部分可以做些强制要求,主要就是利用 coverageThreshold
配置项区别配置
自动化测试
自动化测试是一种使用特定的自动化工具来控制测试执行和比较预期结果和实际结果的过程。它可以减少手动测试的需求,提高测试的效率和覆盖率。
前端自动化测试主要包括以下几种类型:
1. 单元测试:测试应用程序的最小可测试部分。例如,一个函数或者一个组件。
2. 集成测试:测试应用程序中多个组件或模块如何协同工作。
3. 端到端测试(E2E测试):模拟用户行为,测试整个应用程序的工作流程。
在JavaScript中,常用的前端自动化测试工具有Jest(用于单元测试和集成测试)和 Cypress(用于E2E测试)
不过 Cypress
只支持 js,一般来讲这个方向最终还是要交给测试团队的,这里还是建议使用 selenium
简单的例子可以参考 selenium-webdriver – npm (npmjs.com)
由于自动化测试需要编写大量代码,并不是所有的项目都适合自动化测试,适合自动化测试的场景一般有如下要求:
- 需求变更有计划且频率不高
- 项目周期比较长且稳定
- 脚本复用率高
- 代码、环境等可量化
- 项目可以区分手动和自动模块
- …
UI 测试
一般也都是人工测试,很少有项目适合上一堆工具来处理这个问题的,感兴趣的可以研究下BackstopJS
移动端测试最头疼的问题莫过于一像素问题,BackstopJS
也可以测试出来
开发方法
关于测试你可能还需要了解相应的开发方法
TDD
TDD,全称为测试驱动开发(Test-Driven Development),是一种软件开发方法,它强调在编写代码之前先编写测试用例。TDD的基本流程如下:
- 先编写一个简单的测试用例
- 编写最简单的代码使得测试通过
- 重构代码,使其更好,更易于理解,同时保证测试依然通过
- 重复上诉过程
这种方法的优点是可以帮助开发者更好地理解需求,提高代码质量,同时也使得代码更易于维护和扩展。
BDD
BDD,全称为行为驱动开发(Behavior Driven Development),是一种敏捷软件开发的技术。它强调软件项目中开发者、质量保证以及非技术或业务参与者之间的交流。BDD的主要目标是通过持续对软件的预期行为的描述来提高软件的质量和开发过程的效率。
BDD的主要步骤包括:
-
描述业务行为:这通常通过用户故事或场景来完成,描述了软件应该如何行为
-
编写失败的测试:这是测试驱动开发(TDD)的一部分,开发者首先编写一个失败的测试来描述新的行为
-
实现行为:开发者编写代码来使测试通过,从而实现新的行为
-
重构:开发者重构代码,保持行为不变,但改进结构和实现
-
重复上述过程
BDD的主要优点是它帮助团队专注于软件的行为,而不仅仅是技术细节。这有助于确保软件满足业务需求,并且易于理解和维护。
DDD
DDD (Domain-Driven Design) 是一种软件开发方法,它主要关注的是核心业务逻辑,也就是领域模型。这种方法强调的是,软件的主要复杂性来自于领域本身,而不是技术。因此,它主张使用一种通用的、基于领域特定语言的模型来驱动软件的设计和实现。
DDD 的主要组成部分包括:
– 实体(Entity):具有唯一标识的对象。
– 值对象(Value Object):没有唯一标识的对象,通过其属性来定义。
– 聚合(Aggregate):一组实体和值对象的集合,它们作为一个整体进行持久化。
– 领域事件(Domain Event):表示领域中的某个重要事件。
– 服务(Service):在领域中执行重要业务逻辑的操作,通常是无法归类到实体或值对象的操作。
– 仓储(Repository):提供对聚合的存储和检索。
DDD 的目标是创建一个可以反映业务领域复杂性的模型,这个模型可以用来驱动软件的设计和实现。
在实际开发过程中,可根据实际情况选择不同开发规范
最后
本篇相关代码可查看 vue3-template 项目