Vue进阶(八十八)前端测试工具介绍
文章目录
- 一、前言
- 1.1 引入
- 1.2 基础语法
- 1.2.1 全局函数 describe 和 it
- 1.2.2 断言 expect
- 1.2.3 匹配器
- 1.2.4 snapshot 快照
- 1.2.5 测试用例覆盖率报告
- 1.2.6 React Testing Library render
- 1.2.7 screen
- 1.2.8 查询函数
- 1.2.9 waitFor
- 1.2.10 fireEvent 和 userEvent
- 二、Jest 基本用法和示例
- 2.1 工具支持
- 2.2 配置拓展
- 2.3 Jest 测试案例
- 三、Jest 高级特性和最佳实践
- 四、项目实战 Vue项目集成Jest进行单元测试
- 4.1 浅渲染
- 4.2 应用全局插件和混入
- 4.3 仿造注入
- 4.4 处理路由
- 4.5 项目实战
- 五、拓展阅读
一、前言
Jest 是由 Facebook 提供的开源 JavaScript
测试框架,特别适用于React
和Node.js
环境。它以简单的配置、高效的性能和易用性而闻名,旨在简化前端开发中的单元测试、集成测试、端到端测试和快照测试。Jest 提供了一套完整的测试解决方案,包括断言库、测试运行器、模拟工具等。此外,Jest还提供内置的代码覆盖率工具,帮助开发者优化测试范围,使得编写和运行测试变得更加简单和高效。
Jest 的一些主要特点和优势包括:
- 开箱即用:Jest 内置了断言库、测试覆盖率报告等功能,无需额外配置即可开始编写测试。
- 易于上手:Jest 提供了类似于 BDD(行为驱动开发)的语法,使用
describe
、it
等函数来组织测试,非常直观和易读。 - 快照测试:Jest 支持对 React 组件进行快照测试,可以轻松捕获组件的结构变化,确保组件在修改后仍按预期渲染。
- 模拟和间谍:Jest 内置了强大的模拟和间谍功能,可以模拟模块、函数行为,并监视函数调用情况,方便进行单元测试和集成测试。
- 并行执行:Jest 支持并行执行测试用例,充分利用多核 CPU,加快测试速度。
- 智能提示:Jest 与主流的 IDE 和编辑器(如 VSCode)集成,提供智能提示和自动补全,提高开发效率。
1.1 引入
要在项目中使用 Jest,首先需要通过 npm
或 yarn
安装 Jest 依赖包。可以在项目根目录下运行以下命令:
npm install --save-dev jest
或
yarn add --dev jest
安装完成后,可以在 package.json
文件的 scripts
字段中添加 Jest
的测试命令,例如:
{"scripts": {"test": "jest"}
}
Jest 的配置文件通常命名为 jest.config.js
或 jest.config.ts
,放置在项目根目录下。在配置文件中,可以自定义 Jest 的行为,如测试文件的匹配模式、测试环境的设置、测试覆盖率的阈值等。一个简单的 jest.config.js
文件示例如下所示:
module.exports = {testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],testEnvironment: 'jsdom',coverageThreshold: {global: {branches: 80,functions: 80,lines: 80,statements: -10,},},
};
jest.config.ts
是一个使用 TypeScript 编写的 Jest 配置文件。可以使用npx jest --init
初始化命令来生成一个基本的配置文件。
export default {// 自动清除 mock 调用和实例clearMocks: true,// 开启代码覆盖率收集collectCoverage: true,// 定义代码测试覆盖率通过分析哪些文件生成的,!代表不要分析collectCoverageFrom: ['**/*.{ts,js,tsx}', '!**/node_modules/**', '!**/vendor/**'],// 代码覆盖率报告的输出目录coverageDirectory: 'coverage',// 代码覆盖率的收集器,这里使用 V8 引擎coverageProvider: 'v8',// 代码覆盖率报告的格式coverageReporters: ['text-summary','lcov',],globals: {'ts-jest': {// 关闭 ts-jest 的诊断信息diagnostics: false,},},// 引入模块时,进行自动查找模块类型,逐个匹配moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],// 模块名字使用哪种工具进行映射moduleNameMapper: {'^@/(.*)$': '<rootDir>/src/$1', //将 @/ 映射到 src/ 目录'\\.(css|less)$': 'jest-transform-stub','^localTypes$': '<rootDir>/src/types.ts','^localUtils$': '<rootDir>/src/utils/index.ts','^localConst$': '<rootDir>/src/utils/constants.ts','^Assets/(.*)$': '<rootDir>/assets/$1',},preset: 'ts-jest',rootDir: undefined,// 检测从哪个目录开始,rootDir 代表根目录roots: ['<rootDir>/src'],// 在运行测试之前执行的文件(设置测试环境)setupFilesAfterEnv: ['./setupTests.js'],// 测试运行的环境,会模拟 domtestEnvironment: 'jsdom',// 哪些文件会被认为测试文件testMatch: [// src 下的所有 __tests__ 文件夹中的所有的 js jsx ts tsx 后缀的文件都会被认为是测试文件'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',// scr 下的所有以 .test/spec.js/jsx/ts/tsx 后缀的文件都会被认为是测试文件'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',],// 测试时忽略的路径testPathIgnorePatterns: ['\\\\node_modules\\\\'],// 测试文件中引用一下后缀结尾的文件会使用对应的处理方式transform: {'^.+\\.(t|j)s$': 'ts-jest','\\.svg$': '<rootDir>/__Mock__/svgTransform.js',},
}
.babelrc
配置文件
当使用 Jest 测试一个使用 Babel 编译的项目时,Jest 会通过这些配置来正确处理和理解 JavaScript 代码。
{// 设置插件集合"presets": [// 使用当前插件,可以进行转换// 数组的第二项为插件的配置项["@babel/preset-env",{// 根据 node 的版本号来结合插件对代码进行转换"targets": {"node": "current"}}]]
}
1.2 基础语法
1.2.1 全局函数 describe 和 it
describe
用于将测试分组,而 it
用于定义单个具体的测试用例。可以在 describe
块中放置多个 it 测试用例,也可以嵌套其他 describe
块以创建更详细的测试结构。
// 用于创建一个测试套件,将一组功能或逻辑相关的测试用例组织在一起
describe('测试输入框的校验规则', () => {// it 的第一个参数是一个字符串,描述了测试用例应该做什么,有助于代码的可读性和测试结果的理解it('输入正常', async () => {// ...});it('必填', async () => {// ...});it('仅支持汉字、字母、数字和-_%.', async () => {// ...})it('以数字、字母或汉字开头', async () => {// ...})it('限长', async () => {// ...})
});
1.2.2 断言 expect
用于验证代码的行为是否符合预期。 expect 函数接受一个参数———想要测试的值。然后,expect 返回一个“期望对象”,这个对象提供了一系列“匹配器”(matcher
)方法,用于声明对这个值的期望。
describe('测试输入框的校验规则', () => {it('必填', async () => {// ...expect(message).toBeInTheDocument()})it('仅支持汉字、字母、数字和-_%.', async () => {// ...expect(message).toBeInTheDocument()})it('以数字、字母或汉字开头', async () => {// ...expect(message).toBeInTheDocument()})it('限长', async () => {// ...expect(message).toBeInTheDocument()})it('输入正常', async () => {// ...await waitFor(() => {expect(input.className).toMatch('ant-input-status-success')})})
})
1.2.3 匹配器
-
toBe
:期待是否与匹配器中的值相等,相当于object.is ===
; -
toMatch
:匹配当前字符串中是否含有这个值,支持正则; -
toContain
:用于检查数组或字符串是否包含特定项或子串; -
toBeInTheDocument
:判断某个元素是否在文档中,即是否已被渲染到 DOM 上; -
toHaveProperty
:用于检查对象是否具有特定属性,可以选择性地检查属性值; -
toEqual
:是“相等”,不是“相同”,相当于==
; -
toBeFalsy
和toBeTruthy
:检查一个值是否为假或真; -
toBeNull
:专门用来检查一个值是否为null
; -
toBeDefined
和toBeUndefined
:这些断言用于检查变量是否已定义或未定义; -
toThrow
:用于检查函数是否抛出错误; -
not
:用于对断言取反;
1.2.4 snapshot 快照
会在当前测试文件位于的文件夹下生成一个__snapshots__
文件夹,该文件夹下会生成扩展名为 .snap
文件,文件会保存代码运行的结果(如渲染的组件树、数据结构等)。
toMatchSnapshot
方法:接受一个参数是快照名称,字符串类型。
expect(container).toMatchSnapshot('必填')
注意⚠️:一定要是 container
,不能是 screen
,用 screen
不会保存 DOM
结构!
优势
-
自动化比较:
Jest
自动比较快照,减少了手动检查输出的需要。 -
简化复杂结构的测试:对于复杂对象或大型UI组件,编写传统测试断言可能很困难。快照测试可以轻松捕获整个结构。
-
文档化变化:快照文件也可以作为代码行为的一种文档,让开发者和审阅者理解代码更改的影响。
-
快照更新:当代码发生更改,导致快照不再匹配时,可以使用
jest --updateSnapshot
命令或jest -u
命令来更新快照。
1.2.5 测试用例覆盖率报告
会在主文件夹下生成一个名为 coverage
的文件夹,打开里面的 html
就可以看到各个文件的覆盖率,通常包含以下几种主要的覆盖率类型:
-
行覆盖率(Line Coverage):测量有多少行代码被测试用例执行过。如果一行代码在测试中至少被执行一次,那么这一行就被认为是已覆盖。
-
函数覆盖率(Function Coverage):测量有多少个函数或方法被测试用例调用过。即使函数内的某些行没有被执行,只要函数被调用,它就被认为是已覆盖。
-
分支覆盖率(Branch Coverage):测量代码中的每个if语句、循环、switch语句等的每个分支是否都被执行过。这是检查条件语句完整性的重要指标。
-
语句覆盖率(Statement Coverage):测量有多少个独立语句被测试执行过。这与行覆盖率类似,但关注的是语句的执行。
1.2.6 React Testing Library render
渲染 React 组件到一个虚拟的 DOM 环境中以便进行测试。
render
函数接受一个 React 组件作为参数,并返回一个包含多个属性和方法的对象,例如 container
和 debug
。 container
可以调用各类查询函数在渲染的组件中查找元素, debug
可以打印出 baseElement
的内部HTML,用于调试。
describe('测试输入框的校验规则', () => {it('输入正常', async () => {const Com = <Index />const container = render(Com)container.debug()})
})
1.2.7 screen
在使用 React Testing Library 进行测试时,通常会先用 render 函数渲染组件,然后用 screen 查询和操作元素。screen 对象可以在测试文件中全局访问,无需在每个测试中单独导入或创建。
describe('测试输入框的校验规则', () => {it('输入正常', async () => {render(<Index />)screen.debug()})
})
1.2.8 查询函数
React Testing Library 提供了一系列的查询函数,用于在 Jest 测试中找到 DOM 节点。
getBy…
getByText
: 根据文本内容查找元素。
-
getByLabelText
: 根据关联的 -
getByPlaceholderText
: 根据占位符文本查找输入框。 -
getByAltText: 根据图片的 alt 属性文本查找图片元素。
-
getByTitle: 根据 title 属性查找元素。
-
getByRole: 根据 ARIA 角色查找元素。
-
getByTestId: 根据 data-testid 属性查找元素。
queryBy…
queryBy…函数的行为类似于 getBy… 函数,但当查询的元素不存在时,它们返回 null 而不是抛出错误。这对于断言某个元素不在页面上非常有用。
findBy…
findBy…函数是 getBy… 函数的异步版本。它们返回一个 Promise,适用于等待异步操作完成后元素出现在 DOM 中的情况。
…AllBy…, queryAllBy…, findAllBy…
这些函数的行为类似于 getBy…, queryBy…, 和 findBy…,但用于返回多个匹配的元素。如果没有找到匹配的元素,getAllBy… 和 findAllBy… 会抛出错误,而 queryAllBy… 返回一个空数组。
总结:
getBy… 函数用于当确定元素存在时。如果元素不存在,测试将失败。
queryBy… 函数用于当元素可能不存在,需要处理这种情况时。
findBy… 函数用于处理异步逻辑,当需要等待元素出现时。
…AllBy… 函数用于处理有多个匹配元素的情况。
// findByText参数必须是完整的文本,如果是子字符串,需要加上{exact: false}
// findByText不管前缀是screen还是container都可以成功
describe('测试输入框的校验规则', () => {it('仅支持汉字、字母、数字和-_%.', async () => {const Com = <Index />const container = render(Com)const input = await screen.findByRole('textbox')await userEvent.type(input, '@')const messages = await container.findByText('溶剂名称仅支持汉字、字母、数字和-_%.')})
})
describe('测试输入框的校验规则', () => {it('仅支持汉字、字母、数字和-_%.', async () => {const Com = <Index />const container = render(Com)const input = await screen.findByRole('textbox')await userEvent.type(input, '@')const messages = await screen.findByText('仅支持汉字、字母、数字和-_%.', {exact: false})})
})
1.2.9 waitFor
用于处理异步操作和元素的异步更新。waitFor 常与异步查询函数(如 findBy…)结合使用,用于处理组件状态更新或数据加载。
describe('测试输入框的校验规则', () => {it('输入正常', async () => {const container = render(<Index />)screen.debug()const input = await screen.findByRole('textbox')await waitFor(() => {expect(screen.getByText('必填', { exact: false })).toBeInTheDocument()})})
})
1.2.10 fireEvent 和 userEvent
Jest 提供fireEvent
和userEvent
模拟用户操作。
-
fireEvent
:直接同步触发 DOM 事件。当调用fireEvent
的任何方法时(如 fireEvent.click),它会立即生成对应的 DOM 事件,并同步地传递给目标元素。因此,fireEvent 方法调用后不会返回 Promise,也不涉及任何异步操作,所以通常不需要使用 await 关键字。 -
userEvent
:旨在更贴近用户的实际操作,因此它经常涉及到一系列复杂的、可能是异步的事件。例如,当用户在输入框中输入文字时,这不仅仅是一个简单的同步操作。它包含了一系列的键盘和输入事件,这些事件可能会触发各种事件处理器,这些处理器本身可能是异步的。
1、fireEvent来自’@testing-library/react’,userEvent来自@testing-library/user-event
2、fireEvent的清空 Input 输入框操作为fireEvent.change(input, {target: {value: ‘’}}),userEvent的清空 Input 输入框操作为userEvent.type(input, ‘{backspace}’)
3、fireEvent前不需要添加await,userEvent需要。
总结:如果需要模拟简单的事件并需要完全控制这些事件的属性,fireEvent 是个好选择。而如果需要模拟更复杂或更接近真实用户行为的交互,userEvent 则更合适。
describe('测试输入框的校验规则', () => {it('仅支持汉字、字母、数字和-_%.', async () => {const Com = <Index />const container = render(Com)const input = await screen.findByRole('textbox')fireEvent.change(input, {target: {value: '@'}})})
})
describe('测试输入框的校验规则', () => {it('仅支持汉字、字母、数字和-_%.', async () => {const Com = <Index />const container = render(Com)const input = await screen.findByRole('textbox')await userEvent.type(input, '@')})
})
二、Jest 基本用法和示例
2.1 工具支持
Jest同时提供丰富的工具支持,涉及的安装包如下:
-
jest:这是
Jest
测试框架本身。 -
@types/jest
:这是Jest
的TypeScript
类型定义,用于在使用TypeScript
编写测试时提供类型检查和自动完成功能。 -
babel-jest
:这是用于将Jest
集成到使用Babel
项目中的插件。它允许Jest
处理通过Babel
转换的代码。 -
ts-jest
:这是一个Jest
转换器,用于处理TypeScript
文件。它基本上允许Jest
理解和运行TypeScript
测试代码。 -
jest-transform-stub
:这个插件用于处理非JavaScript
资源(如 CSS 和图片)的导入,这在 Jest 测试中通常会被忽略或需要特殊处理。
npm install --save-dev jest @types/jest babel-jest ts-jest jest-transform-stub
-
@testing-library/jest-dom
:提供一套针对DOM
元素的Jest
断言,非常适用于在测试React
组件时使用。 -
@testing-library/react
:用于测试React
组件,它提供了渲染组件、查询 DOM 元素以及与组件交互的工具。 -
@testing-library/user-event
:这个库用于模拟用户事件(如点击、输入等),可用于更逼真地测试用户交互。
npm install --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event
-
eslint-plugin-jest
:这是一个ESLint
插件,提供针对Jest
测试的特定规则,有助于保证测试代码的质量和一致性。 -
react-test-renderer:一个用于渲染
React
组件为JavaScript
对象库,常用于 Jest 快照测试。它可以在不需要 DOM 环境的情况下测试React
组件输出,这对于在 Node 环境下运行 Jest 测试非常有用。
npm install --save-dev eslint-plugin-jest react-test-renderer
2.2 配置拓展
前面小结讲到,Jest 安装完成后,在 package.json
文件的 scripts
字段中添加 Jest
测试命令之后,便可进行简单的Jest测试,例如:
{"scripts": {"test": "jest"}
}
npm run test
除了基础配置,还可以指定特殊配置以满足不同需求。
package.json
配置文件如下:
--watchAll
:这个参数告诉 Jest 进入 “watch” 模式。在这个模式下, Jest 会监视项目中的文件变化。当修改并保存了代码文件(包括测试文件和被测试的源代码文件)时, Jest 会自动重新运行相关的测试。
--watchAll
与 --watch
不同之处在于,--watchAll
会在初次运行时执行所有测试,而 --watch
只在检测到文件更改时运行相关测试。
"test": "jest --watchAll",
运行某个文件夹下的所有测试文件,src/tests
代表文件夹路径。
"test:folder": "jest --watchAll --testPathPattern=src/tests",
单独运行某个测试文件,src/renderer/login/loginApi.test.tsx
代表需要测试的文件路径。
"test:single": "jest --watchAll jest --findRelatedTests src/renderer/login/loginApi.test.tsx",
在 Jest 中,测试代码通常组织为测试套件(Test Suite
)和测试用例(Test Case
)。测试套件用于对相关测试用例进行分组,通常使用 describe
函数来定义。测试用例则是具体的测试场景,使用 it
或 test
函数来定义。每个测试用例中,可以使用断言(Assertion)来验证被测代码的行为是否符合预期。Jest 提供了丰富的断言函数和匹配器(Matcher),如 expect
、toBe
、toEqual
等,用于进行值的比较和判断。
除了断言外,Jest 还提供了模拟(Mock
)和间谍(Spy
)功能,用于隔离被测代码的依赖,控制函数的行为和监视函数的调用情况。通过模拟和间谍,可以创建仿真对象和函数,模拟异步操作、文件系统等,使得测试更加可控和独立。
在 Jest 中编写测试用例非常简单,使用 test()
或 it()
函数即可定义一个测试用例。这两个函数的用法相同,都接受两个参数:一个描述测试用例的字符串和一个包含测试代码的回调函数。在测试用例中,使用断言函数(如 expect()
)和匹配器(如 toBe()
、toEqual()
)来验证被测代码的行为是否符合预期。
以下是一个简单的测试用例示例:
test('adds 1 + 2 to equal 3', () => {expect(1 + 2).toBe(3);
});
当被测代码包含异步操作时,Jest 提供了几种方式来测试异步代码。一种方式是使用回调函数和 done()
参数。在测试用例的回调函数中,调用 done()
表示异步操作完成。Jest 会等待 done()
被调用后才结束测试用例。
另一种测试异步代码的方式是使用 Promise
。可以在测试用例中返回一个 Promise
,并使用 resolves
或 rejects
匹配器来验证 Promise
的状态和结果。
如果使用 ES2017 的 async/await
语法,可以像编写同步代码一样编写异步测试用例,Jest 会自动处理异步操作。
下面是一个异步测试用例的示例:
test('fetches data from API', async () => {const data = await fetchData();expect(data).toEqual({ id: 1, name: 'John' });
});
Jest 还提供了测试 React 组件的功能。可以使用 render()
函数将组件渲染为虚拟 DOM,并使用 screen
对象查询渲染后的元素。通过 fireEvent
工具,可以模拟用户的交互事件,如点击、输入等。
此外,Jest 支持对 React 组件进行快照测试。快照测试可以捕获组件的结构,并将其与之前保存的快照进行比较,以确保组件在修改后仍然按预期渲染。
以下是一个 React 组件的测试示例:
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';test('renders and updates correctly', () => {render(<MyComponent />);expect(screen.getByText('Hello, World!')).toBeInTheDocument();fireEvent.click(screen.getByText('Click Me'));expect(screen.getByText('Button Clicked')).toBeInTheDocument();expect(screen.getByTestId('my-component')).toMatchSnapshot();
});
对于使用 Redux
进行状态管理的应用,Jest 可以用于测试 Action Creator 和 Reducer。通过创建模拟的 Redux 存储,可以测试 Action
是否正确派发以及 Reducer
是否正确更新状态。可以使用 redux-mock-store
库来模拟 Redux
存储,并检查 Action
的派发情况。
以下是一个 Redux Reducer 的测试示例:
import reducer from './reducer';
import * as actions from './actions';test('should handle ADD_TODO', () => {const initialState = [];const newTodo = { id: 1, text: 'New Todo' };const newState = reducer(initialState, actions.addTodo(newTodo));expect(newState).toEqual([newTodo]);
});
通过这些示例,可以看到 Jest 提供了丰富的功能和工具,使得编写各种类型的测试变得简单和直观。无论是同步代码、异步代码、React 组件还是 Redux 状态管理,Jest 都能够很好地满足测试需求,提高代码的质量和可维护性。
2.3 Jest 测试案例
测试 Input 输入框的校验规则
当前 Input 输入框的校验规则:
(1)必填
(2)限长100
(3)仅支持汉字、字母、数字和-_%.
(4)必须以数字、字母或汉字开头
const nameRules = ({label,max = 10,required = true,
}: {label: stringmax?: numberrequired?: boolean
}): Rule[] => [{ required, message: `${label}必填` },{ type: 'string', max, message: `${label}限长${max}` },{pattern: /^([a-zA-Z0-9\u4E00-\u9FA5_.%-])*$/g,message: `${label}仅支持汉字、字母、数字和-_%.`,},{pattern: /^([0-9|a-zA-Z0-9|\u4E00-\u9FA5])/g,message: `${label}以数字、字母或汉字开头`,},]
因被测试组件的复杂程度不同,测试同一个功能所用的 API 也不同。
(1)被测试功能组件的简单版:
该组件只有基本的页面布局和nameRules校验规则
/* eslint-disable react-hooks/rules-of-hooks */
import { nameRules } from '@/utils/constants'
import { Form, Input } from 'antd'const myInput = () => {return (<Form><Form.Itemlabel="Username"name="username"// 校验规则rules={nameRules({label: '名称',required: true,})}><Input /></Form.Item></Form>)
}
export default myInput
在测试较简单的组件时,模拟用户操作可以使用fireEvent.change(),断言也无需包裹在waitFor中便可同步执行。
/* eslint-disable no-undef */
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import Index from './index'
import '@testing-library/jest-dom'describe('测试输入框的校验规则', () => {it('必填', async () => {// 渲染组件const Com = <Index />const container = render(Com)// findByRole不管前缀是screen还是container都可以成功const input = await screen.findByRole('textbox')// 在 input 输入框中输入1fireEvent.change(input, { target: { value: '1' } })// 清空 inputfireEvent.change(input, { target: { value: '' } })// findByText参数必须是完整的文本,如果是子字符串,需要加上{exact: false}expect(await container.findByText('必填', { exact: false })).toBeInTheDocument()})it('仅支持汉字、字母、数字和-_%.', async () => {const Com = <Index />const container = render(Com)const input = await screen.findByRole('textbox')fireEvent.change(input, { target: { value: '@' } })expect(await container.findByText('仅支持汉字、字母、数字和-_%.', { exact: false })).toBeInTheDocument()})it('以数字、字母或汉字开头', async () => {const Com = <Index />const container = render(Com)const input = await screen.findByRole('textbox')fireEvent.change(input, { target: { value: '-' } })expect(await container.findByText('以数字、字母或汉字开头', { exact: false })).toBeInTheDocument()})it('限长', async () => {const Com = <Index />const container = render(Com)const input = await screen.findByRole('textbox')fireEvent.change(input, { target: { value: 'a'.repeat(101) } })expect(await container.findByText('限长', { exact: false })).toBeInTheDocument()})it('输入正常', async () => {const Com = <Index />const container = render(Com)const input = await screen.findByRole('textbox')fireEvent.change(input, { target: { value: '1' } })await waitFor(() => {expect(input.className).toMatch('ant-input-status-success')})})
})
(2)被测试功能组件的复杂版:
该组件是个集合组件,功能比较复杂,被测试的输入框只是其中一小部分内容。
因为组件存在 fetch 接口的请求,但是 jest 测试不会运行真实的 fetch 接口,所以需要 mock 数据,在本组件中通过在catch中给定初始数据。
在复杂环境下render组件时,需要 mock 渲染组件所需的各项参数,在本组件中id值是直接给定一个存在的 id ,onCancel方法 mock 一个空函数,Dn初始化数据。
此时模拟用户操作须使用await userEvent.type()
,断言外须包裹await waitFor(() => {})
。
/* eslint-disable no-undef */
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import ComplexIndex from './ComplexIndex'
import '@testing-library/jest-dom'
import userEvent from '@testing-library/user-event'describe('测试输入框的校验规则', () => {const onCancelMock = jest.fn()it('必填', async () => {// 渲染组件render(<ComplexIndexid="93e"onCancel={onCancelMock}Dn={{dn1: 1,dn2: '',}}/>)const input = await screen.findByRole('textbox')// 在 input 输入框中输入“正常输入”await userEvent.type(input, '1')// 清空 inputawait userEvent.type(input, '{backspace}')// 异步等待断言执行await waitFor(() => {expect(screen.getByText('必填', { exact: false })).toBeInTheDocument()})})it('正常输入', async () => {render(<ComplexIndexid="93e"onCancel={onCancelMock}Dn={{dn1: 1,dn2: '',}}/>)const input = await screen.findByRole('textbox')await userEvent.type(input, '正常输入')await waitFor(() => {expect(input.className).toMatch('ant-input-status-success')})})it('限长', async () => {render(<ComplexIndexid="93e"onCancel={onCancelMock}Dn={{dn1: 1,dn2: '',}}/>)const input = await screen.findByRole('textbox')await userEvent.type(input, 'a'.repeat(101))await waitFor(() => {expect(screen.getByText('限长', { exact: false })).toBeInTheDocument()})})it('仅支持汉字、字母、数字和-_%.', async () => {render(<ComplexIndexid="93e"onCancel={onCancelMock}Dn={{dn1: 1,dn2: '',}}/>)const input = await screen.findByRole('textbox')await userEvent.type(input, '@')await waitFor(() => {expect(screen.getByText('仅支持汉字、字母、数字和-_%.', { exact: false })).toBeInTheDocument()})})it('以数字、字母或汉字开头', async () => {render(<ComplexIndexid="93e"onCancel={onCancelMock}Dn={{dn1: 1,dn2: '',}}/>)const input = await screen.findByRole('textbox')await userEvent.type(input, '-')await waitFor(() => {expect(screen.getByText('以数字、字母或汉字开头', { exact: false })).toBeInTheDocument()})})
})
(3)获取原始 DOM 内容进行测试
Input 标签有aria-describedby
属性,该属性的属性值是某个div
的id,该div下的div包含所有类型的报错字样。
/* eslint-disable no-undef */
import { fireEvent, render, screen } from '@testing-library/react'
import Index from './index'
import '@testing-library/jest-dom'
import userEvent from '@testing-library/user-event'describe('测试输入框的校验规则', () => {it('仅支持汉字、字母、数字和-_%.', async () => {// 渲染被测组件const Com = <Index />const container = render(Com)// 获取input元素const input = await screen.findByRole('textbox')// 在input输入框中输入@await userEvent.type(input, '@')// 获取input元素const inputEl = document.querySelector("input[type='text']")// 获取input元素的所有属性const attributes = inputEl!.attributeslet ariaDescribedby = ''for (let i = 0; i < attributes?.length; i++) {console.log(attributes[i].name, attributes[i].value)// 找到aria-describedby属性if (attributes[i].name === 'aria-describedby') {// 获取 aria-describedby 属性的值ariaDescribedby = attributes[i].value}}// div 的 id 值为 aria-describedby 属性的值const borderDiv = document.getElementById(ariaDescribedby)const childrenDiv = borderDiv?.querySelectorAll('div')childrenDiv?.forEach(div => {// 报错文本console.log(div.textContent)})})
})
三、Jest 高级特性和最佳实践
Jest 提供了一些高级特性和最佳实践,可以帮助我们编写更加高效、可维护的测试代码。
首先,Jest 提供了钩子函数,用于在测试用例执行的不同阶段执行设置和清理操作。常用的钩子函数包括 beforeEach()
、afterEach()
、beforeAll()
和 afterAll()
。beforeEach()
和 afterEach()
会在每个测试用例执行前后被调用,而 beforeAll()
和 afterAll()
则在所有测试用例执行前后被调用一次。通过使用钩子函数,我们可以在测试之间共享设置和清理代码,避免重复编写相同的逻辑。
Jest 还提供了强大的模块模拟功能,允许模拟外部模块的行为,以便隔离被测代码的依赖。使用 jest.mock()
函数可以模拟整个模块,并指定模拟的实现。使用 jest.fn()
可以创建一个模拟函数,用于跟踪函数的调用情况和返回值。使用 jest.spyOn()
可以监视真实模块中的函数调用,并在测试后恢复原有的实现。通过模块模拟,可以控制外部依赖的行为,使测试更加可控和稳定。
代码覆盖率是衡量测试质量的重要指标,Jest 内置了生成代码覆盖率报告的功能。通过在 Jest 配置文件中启用覆盖率收集,并运行测试命令,Jest 会自动生成详细的代码覆盖率报告,包括语句覆盖率、分支覆盖率、函数覆盖率和行覆盖率等。可以通过配置覆盖率阈值,确保测试覆盖率达到一定的标准。此外,对于一些不需要测试的文件和代码块,可以使用注释或配置文件进行排除,以提高覆盖率的准确性。
在编写 Jest 测试时,遵循一些最佳实践和技巧可以提高测试的可读性和可维护性。以下是一些建议:
- 合理组织和命名测试文件,通常将测试文件与被测代码文件放在同一目录下,并以
.test.js
或.spec.js
作为文件扩展名。 - 使用描述性的测试用例名称,清晰表达测试的目的和预期行为,如
'should return the correct result when given valid input'
。 - 保持测试用例的独立性和可重复性,每个测试用例应该能够独立运行,不依赖于其他测试用例的执行顺序或状态。
- 使用工厂函数和辅助函数来简化测试代码,抽象出通用的设置和断言逻辑,提高测试可读性和可维护性。
示例代码:
// 使用钩子函数共享设置和清理代码
beforeEach(() => {// 在每个测试用例执行前进行设置jest.resetModules();jest.clearAllMocks();
});afterEach(() => {// 在每个测试用例执行后进行清理cleanup();
});// 使用模块模拟
jest.mock('./api');
import { fetchData } from './api';test('should fetch data successfully', async () => {fetchData.mockResolvedValue({ id: 1, name: 'John' });const result = await someFunction();expect(result).toEqual({ id: 1, name: 'John' });expect(fetchData).toHaveBeenCalledTimes(1);
});// 使用工厂函数简化测试代码
function createTestUser(overrides) {return {id: 1,name: 'John',email: 'john@example.com',...overrides,};
}test('should update user profile', () => {const user = createTestUser({ name: 'John Doe' });const updatedUser = updateProfile(user, { email: 'johndoe@example.com' });expect(updatedUser).toEqual({id: 1,name: 'John Doe',email: 'johndoe@example.com',});
});
通过应用这些高级特性和最佳实践,可以编写更加健壮、可维护的 Jest 测试,提高代码质量和开发效率。
四、项目实战 Vue项目集成Jest进行单元测试
4.1 浅渲染
在测试用例中,我们通常希望专注在一个孤立的单元中测试组件,避免对其子组件的行为进行间接的断言。
额外的,对于包含许多子组件的组件来说,整个渲染树可能会非常大。重复渲染所有的子组件可能会让我们的测试变慢。
Vue Test Utils 允许通过 shallowMount
方法只挂载一个组件而不渲染其子组件 (即保留它们的存根):
import { shallowMount } from '@vue/test-utils'const wrapper = shallowMount(Component)
wrapper.vm // 挂载的 Vue 实例
4.2 应用全局插件和混入
有些组件可能依赖一个全局插件或混入 (mixin) 的功能注入,比如 vuex
和 vue-router
。
如果为一个特定的应用撰写组件,可以在测试入口一次性设置相同的全局插件和混入。但是有些情况下,比如测试一个可能会跨越不同应用共享的普通组件套件时,最好在隔离设置中测试组件,不对全局的 Vue 构造函数注入任何东西。可使用 createLocalVue
方法来存档它们:
import { createLocalVue, mount } from '@vue/test-utils'// 创建一个扩展的 `Vue` 构造函数
const localVue = createLocalVue()// 正常安装插件
localVue.use(MyPlugin)// 在挂载选项中传入 `localVue`
mount(Component, {localVue
})
注意⚠️:有些插件会为全局 Vue 构造函数添加只读属性,比如 Vue Router
。这使得无法在一个 localVue
构造函数上二次安装该插件,或伪造这些只读属性。
4.3 仿造注入
另一个注入 prop
的策略就是简单的仿造它们。可以使用 mocks
选项:
import { mount } from '@vue/test-utils'const $route = {path: '/',hash: '',params: { id: '123' },query: { q: 'hello' }
}mount(Component, {mocks: {// 在挂载组件之前添加仿造的 `$route` 对象到 Vue 实例中$route}
})
4.4 处理路由
因为路由需要在应用的全局结构中进行定义,且引入了很多组件,所以最好集成到 end-to-end 测试。对于依赖 vue-router 功能的独立的组件来说,可使用上面提到的仿造注入技术仿造它们。
4.5 项目实战
安装 Jest
和 Vue Test Utils
:
npm install --save-dev jest @vue/test-utils
接下来在 package.json
里定义一个 test:unit
脚本。
// package.json
{// .."scripts": {// .."test:unit": "jest"}// ..
}
在 Jest 中执行单文件组件
为了讲解 Jest 如何处理 *.vue 文件,我们需要安装并配置 vue-jest 预处理器:
npm install --save-dev vue-jest
然后在 package.json
里创建一个 jest 块:
{// ..."jest": {"moduleFileExtensions": ["js","ts","json",// 告诉 Jest 处理 `*.vue` 文件"vue"],"transform": {// 用 `vue-jest` 处理 `*.vue` 文件".*\\.(vue)$": "vue-jest"},"testURL": "http://localhost/"}
}
为 Jest 配置 TypeScript
为了在测试中使用 TypeScript 文件,我们需要在 Jest 中设置编译 TypeScript。为此我们需要安装 ts-jest:
npm install --save-dev ts-jest
接下来,需要在 package.json 中的 jest.transform 中加入一个入口告诉 Jest 使用 ts-jest 处理 TypeScript 测试文件:
{// ..."jest": {// ..."transform": {// ...// 用 `ts-jest` 处理 `*.ts` 文件"^.+\\.tsx?$": "ts-jest"}// ...}
}
放置测试文件
默认情况下,Jest 将会在整个工程里递归地找到所有的 .spec.js
或 .test.js
扩展名文件。
需要改变 package.json
文件里的 testRegex
配置项以运行 .ts
扩展名的测试文件。
在 package.json 中添加以下 jest 字段:
{// ..."jest": {// ..."testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"}
}
Jest 推荐在被测试的代码旁边创建一个 __tests__
目录,但也可以根据自己的风格组织测试文件。只是要注意 Jest 会在进行截图测试的时候在测试文件旁边创建一个 __snapshots__
目录。
撰写一个单元测试
创建一个 src/components/__tests__/HelloWorld.spec.ts
文件,并加入如下代码:
// src/components/__tests__/HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'describe('HelloWorld.vue', () => {test('renders props.msg when passed', () => {const msg = 'new message'const wrapper = shallowMount(HelloWorld, {propsData: { msg }})expect(wrapper.text()).toMatch(msg)})
})
以上就是 TypeScript
和 Vue Test Utils
一起工作所需要的全部工作
五、拓展阅读
- Jest官网
- Vue Test Utils
- Vue 官方测试指导
相关文章:
Vue进阶(八十八)前端测试工具介绍
文章目录 一、前言1.1 引入1.2 基础语法1.2.1 全局函数 describe 和 it1.2.2 断言 expect1.2.3 匹配器1.2.4 snapshot 快照1.2.5 测试用例覆盖率报告1.2.6 React Testing Library render1.2.7 screen1.2.8 查询函数1.2.9 waitFor1.2.10 fireEvent 和 userEvent 二、Jest 基本用…...
【录制,纯正人声】OBS录制软件,音频电流音,杂音解决办法,录制有噪声的解决办法
速度解决的方法 (1)用RNNoise去除噪声。RNNoise是一个开源的,效果不好的噪声去除器。使用方法就是点击滤镜,然后加噪声抑制RNNoise。【这方法不好用】 (2)用Krisp(https://krisp.ai/) 去除噪声。这个Kris…...
Django中drf动态过滤查询
Django中drf动态过滤查询 1、page.py 代码: from rest_framework.pagination import PageNumberPaginationclass UserPagination(PageNumberPagination):"""用户分页器"""page_size = 10 # 默认的页面数据数量page_query_param = page # 定…...
GTSAM | gtsam::PriorFactor
文章目录 概述一、定义介绍二、功能作用三、主要内容四、实例演示概述 本节介绍了GTSAM中的gtsam::PriorFactor类。 一、定义介绍 gtsam::PriorFactor 是 GTSAM(Graph-based Trajectory and Mapping)库中的一个类,用于定义先验因子。在因子图优化中,先验因子用于将一些变量…...
MMSegmentation改进:增加Kappa系数评价指数
将mmseg\evaluation\metrics\iou_metric.py文件中的内容替换成以下内容即可: 支持输出单类Kappa系数和平均Kappa系数。 使用方法:将dataset的config文件中:val_evaluator 添加mKappa,如 val_evaluator dict(typemmseg.IoUMetri…...
专栏【汇总】
专栏【汇总】 前言版权推荐专栏【汇总】付费 汇总置顶在读在学我的面试计算机重要课程java面试Java基础数据存储Java框架java提高计算机科学与技术课程算法杂项 最后 前言 2024-5-12 21:13:02 以下内容源自《【专栏】》 仅供学习交流使用 版权 禁止其他平台发布时删除以下此…...
成功解决IndexError: index 0 is out of bounds for axis 1 with size 0
成功解决IndexError: index 0 is out of bounds for axis 1 with size 0 🛠️ 成功解决IndexError: index 0 is out of bounds for axis 1 with size 0摘要引言正文内容(详细介绍)🤔 错误分析:为什么会发生IndexError&…...
C# MES通信从入门到精通(11)——C#如何使用Json字符串
前言 我们在开发上位机软件的过程中,经常需要和Mes系统进行数据交互,并且最常用的数据格式是Json,本文就是详细介绍Json格式的类型,以及我们在与mes系统进行交互时如何组织Json数据。 1、在C#中如何调用Json 在C#中调用Json相关…...
ON DUPLICATE KEY UPDATE 子句
ON DUPLICATE KEY UPDATE 是 MySQL 中的一个 SQL 语句中的子句,主要用于在执行 INSERT 操作时处理可能出现的重复键值冲突。当尝试插入的记录导致唯一索引或主键约束冲突时(即试图插入的记录的键值已经存在于表中),此子句会触发一…...
perl use HTTP::Server::Simple 轻量级 http server
cpan -i HTTP::Server::Simple 返回:已是 up to date. 但是我在 D:\Strawberry\perl\site\lib\ 找不到 HTTP\Server 手工安装:下载 HTTP-Server-Simple-0.52.tar.gz 解压 tar zxvf HTTP-Server-Simple-0.52.tar.gz cd D:\perl\HTTP-Server-Simple-…...
【STM32】基于I2C协议的OLED显示(利用U82G库)
【STM32】基于I2C协议的OLED显示(利用U82G库) 文章目录 【STM32】基于I2C协议的OLED显示(利用U82G库)一、实验背景二、U8g2介绍(一)获取(二)简介 三、实践(一)CubexMX配置(二)U8g2配…...
掌握Python3输入输出:轻松实现用户交互、日志记录与数据处理
Python 是一门简洁且强大的编程语言,广泛应用于各个领域。在 Python 编程中,输入和输出是基本而重要的操作。无论是进行用户交互、记录日志信息,还是将计算结果输出到控制台或文件,掌握这些操作都是编写高效 Python 程序的关键。本…...
用于每个平台的最佳WordPress LMS主题
你已选择在 WordPress 上构建学习管理系统 (LMS)了。恭喜! 你甚至可能已经选择了要使用的 LMS 插件,这已经是成功的一半了。 现在是时候弄清楚哪个 WordPress LMS 主题要与你的插件配对。 我将解释 LMS 主题和插件之间的区别,以便你了解要…...
pytorch 加权CE_loss实现(语义分割中的类不平衡使用)
加权CE_loss和BCE_loss稍有不同 1.标签为long类型,BCE标签为float类型 2.当reduction为mean时计算每个像素点的损失的平均,BCE除以像素数得到平均值,CE除以像素对应的权重之和得到平均值。 参数配置torch.nn.CrossEntropyLoss(weightNone,…...
【iOS】UI——关于UIAlertController类(警告对话框)
目录 前言关于UIAlertController具体操作及代码实现总结 前言 在UI的警告对话框的学习中,我们发现UIAlertView在iOS 9中已经被废弃,我们找到UIAlertController来代替UIAlertView实现弹出框的功能,从而有了这篇关于UIAlertController的学习笔记…...
django支持https
测试环境,可以用django自带的证书 安装模块 sudo pip3 install django_sslserver服务端https启动 python3 manage.py runsslserver 127.0.0.1:8001https访问 https://127.0.0.1:8001/quota/api/XXX...
算法题day41(补5.27日卡:动态规划01)
一、动态规划基础知识:在动态规划中每一个状态一定是由上一个状态推导出来的。 动态规划五部曲: 1.确定dp数组 以及下标的含义 2.确定递推公式 3.dp数组如何初始化 4.确定遍历顺序 5.举例推导dp数组 debug方式:打印 二、刷题…...
【附带源码】机械臂MoveIt2极简教程(四)、第一个入门demo
系列文章目录 【附带源码】机械臂MoveIt2极简教程(一)、moveit2安装 【附带源码】机械臂MoveIt2极简教程(二)、move_group交互 【附带源码】机械臂MoveIt2极简教程(三)、URDF/SRDF介绍 【附带源码】机械臂MoveIt2极简教程(四)、第一个入门demo 目录 系列文章目录1. 创…...
基于蚁群算法的二维路径规划算法(matlab)
微♥关注“电击小子程高兴的MATLAB小屋”获得资料 一、理论基础 1、路径规划算法 路径规划算法是指在有障碍物的工作环境中寻找一条从起点到终点、无碰撞地绕过所有障碍物的运动路径。路径规划算法较多,大体上可分为全局路径规划算法和局部路径规划算法两大类。其…...
政务云参考技术架构
行业优势 总体架构 政务云平台技术框架图,由机房环境、基础设施层、支撑软件层及业务应用层组成,在运维、安全和运营体系的保障下,为政务云使用单位提供统一服务支撑。 功能架构 标准双区隔离 参照国家电子政务规范,打造符合标准的…...
android 13 aosp 预置so库
展讯对应的main.mk配置 device/sprd/qogirn**/ums***/product/***_native/main.mk $(call inherit-product-if-exists, vendor/***/build.mk)vendor/***/build.mk PRODUCT_PACKAGES \libtestvendor///Android.bp cc_prebuilt_library_shared{name:"libtest",srcs:…...
mongo篇---mongoDB Compass连接数据库
mongo篇—mongoDB Compass连接数据库 mongoDB笔记 – 第一条 一、mongoDB Compass连接远程数据库,配置URL。 URL: mongodb://username:passwordhost:port点击connect即可。 注意:host最好使用名称,防止出错连接超时。...
基于SOA海鸥优化算法的三维曲面最高点搜索matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于SOA海鸥优化算法的三维曲面最高点搜索matlab仿真,输出收敛曲线以及三维曲面最高点搜索结果。 2.测试软件版本以及运行结果展示 MATLAB2022A版本…...
前端js解析websocket推送的gzip压缩json的Blob数据
主要依赖插件pako https://www.npmjs.com/package/pako 1、安装 npm install pako 2、使用, pako.inflate(reader.result, {to: "string"}) 解压后的string 对象,需要JSON.parse转成json this.ws.onmessage (evt) > {console.log("…...
【wiki知识库】06.文档管理接口的实现--SpringBoot后端部分
目录 一、🔥今日目标 二、🎈SpringBoot部分类的添加 1.调用MybatisGenerator 2.添加DocSaveParam 3.添加DocQueryVo 三、🚆后端新增接口 3.1添加DocController 3.1.1 /all/{ebokId} 3.1.2 /doc/save 3.1.3 /doc/delete/{idStr} …...
c,c++,go语言字符串的演进
#include <stdio.h> #include <string.h> int main() {char str[] {a,b,c,\0,d,d,d};printf("string:[%s], len:%d \n", str, strlen(str) );return 0; } string:[abc], len:3 c语言只有数组的概念,数组本身没有长度的概念,需…...
vue-cli 快速入门
vue-cli (目前向Vite发展) 介绍:Vue-cli 是Vue官方提供一个脚手架,用于快速生成一个Vue的项目模板。 Vue-cli提供了如下功能: 统一的目录结构 本地调试 热部署 单元测试 集成打包上线 依赖环境:NodeJ…...
机器人--矩阵运算
两个矩阵相乘的含义 P点在坐标系B中的坐标系PB,需要乘以B到A到变换矩阵TAB。 M点在B坐标系中的位姿MB,怎么计算M在A中的坐标系? 两个矩阵相乘 一个矩阵*另一个矩阵的逆矩阵...
期末复习【汇总】
期末复习【汇总】 前言版权推荐期末复习【汇总】最后 前言 2024-5-12 20:52:17 截止到今天,所有期末复习的汇总 以下内容源自《【创作模板】》 仅供学习交流使用 版权 禁止其他平台发布时删除以下此话 本文首次发布于CSDN平台 作者是CSDN日星月云 博客主页是ht…...
【IM即时通讯】MQTT协议的详解(3)- CONNACK Packet
【IM即时通讯】MQTT协议的详解(3)- CONNACK Packet 文章目录 【IM即时通讯】MQTT协议的详解(3)- CONNACK Packet前言说明一、固定同步详解、可变头部详解总结 前言 关于所有的类型的数据示例已经在上面一篇博客说完: …...
大学毕业做网站插画师好吗/全国各城市疫情高峰感染进度
背景 我试图用python编写一个基本的字母游戏。在游戏中,计算机管理员从可能的单词列表中选出一个单词。每个玩家(计算机人工智能和人类)都会显示一系列空格,每个字母对应一个单词。然后,每个玩家猜测一个字母和一个位置…...
wordpress网站维护页面模板/sem外包
spark最近出了2.0版本,其安装和使用也发生了些许的变化。笔者的环境为:centos7. 该文章主要是讲述了在centos7上搭建spark2.0的具体操作和spark的简单使用,希望可以给刚刚接触spark的朋友一些帮助。 按照惯例,文章的最后列出了一…...
太原制作网站的公司/今日热点
1.如何将网站从http升级到https? 到CA机构申请SSL证书,将SSL证书部署到服务器端(在服务器上安装SSL证书),这样就可以实现https网站。 2.把网站配置成https的话,客户端一定需要安装证书吗? 如果你购买了SSL证书,只要在服务器上安…...
网站建设内容策略有哪些/广西seo公司
最近写一个采集程序,然后就想到了通用分析网页再采集数据的方法,通过几天的摸索终于写出来了,与博客园的各位分享。 欢迎测试,欢迎指正,欢迎评论。 1 <% 2 asp html解析 3 4 by 吴烈 xWorker.cn5 保留所有权利。…...
北京餐饮培训网站建设/端口扫描站长工具
供应商管理是一个术语,描述企业管理其供应商的过程,这些供应商又被称作供货商。供应商管理包括选择供应商、谈判合同、控制成本、减少与供应商有关的风险以及确保服务交付等活动。 企业使用的供应商因组织性质的不同而有很大不同,可能包括不…...
爱客是什么牌子档次/优化方案的格式及范文
练习题:1、查询显示2、请输入你想要做的操作(1:添加,2:删除,3:修改):3、提示用户操作是否成功,刷新数据,回到2等待用户操作 建表: 1 c…...