前言
单元测试是现代软件开发中不可或缺的一部分。它可以让开发人员更加自信地修改和重构代码,而不会破坏现有的功能。在编写组件时,单元测试可以帮助我们确保组件的正确性和可靠性,并提高代码质量。此外,单元测试还可以帮助我们快速定位和修复代码中的潜在问题和错误,从而提高回归效率,减少调试和修复错误所需的时间和成本。
在Vue3中,我们可以使用一些现成的单元测试框架和工具,如Jest和Vue Test Utils。在写单元测试时,我们可以模拟各种不同的场景和输入,以测试组件在不同情况下的行为和反应。
今天,我们便来了解下如何在Vue3中进行单元测试。
准备工作
3. 勾上unit Testing(单元测试)
4. 选好之后回车,选择Vue版本,这里选择3.x:
5. 选择一个格式化标准,分别是:
只提示错误的ESlint
ESlint+Airbnb config:Airbnb config是由Airbnb公司开发和维护的一组ESLint配置规则和插件,用于帮助开发人员编写符合Airbnb代码风格指南的JavaScript代码
ESLint+Standard config:standard config是基于ESLint的一个预设,它是一组ESLint配置规则和插件,用于帮助开发人员编写符合JavaScript标准代码风格的代码
ESLint+prettier:Prettier是一个代码格式化工具,可以帮助开发人员自动格式化代码,使其符合一致的代码风格
可以根据自己的喜好选择。
接下来选择,什么时候应用这些规则:
Lint on save:保存时检查
Lint and fix on commit:提交时检查
我选择是全部勾上,当然你也可以根据自己的需要来勾选。
7. 选择进行测试的框架:
Jest和Mocha + Chai是两种不同的测试框架,这里我选择的是Jest
选择Babel,ESLint配置内容放的位置:
我选择的是单独的config文件,您也可以根据自己的喜好选择。
9. 回车生成项目。
Jest
如何部署Jest 单元测试
由于我们在创建项目时,勾选了单元测试,所以在项目结构中,我们已经可以看见est.config.js以及tests目录下的example.spec.js
通过运行npm run test:unit 执行example.spec.js中的测试。
接下来我们来学习下 jest 的相关配置
jest 的相关配置
首先,我们来看下jest.config.js中的内容:
module.exports = { preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', transform: { '^.+\\.vue$': 'vue-jest', }, }
其中: preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',表示预设的规则是typescript-and-babel的规则。 transform: { '^.+\\.vue$': 'vue-jest', },表示编译时,遇到.vue后缀使用vue-jest进行转换。 这里的我们要注意的重点还是preset预设的规则有哪些。
在guthub上找到vue-cli的代码,进入package目录:
再进入 @vue 目录下: 找到 cli-plugin-unit-jest 目录:
进入preset目录:
选择typescript-and-babel
可以看到:
打开jest-preset.js:
这里我们主要关注的还是defaultTsPreset的内容,找到对应目录下的defaultTsPreset内容:
module.exports = { testEnvironment: 'jsdom', moduleFileExtensions: [ 'js', 'jsx', 'json', // tell Jest to handle *.vue files 'vue' ], transform: { // process *.vue files with vue-jest '^.+\\.vue$': vueJest, '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4| webm|wav|mp3|m4a|aac|oga|avif)$': require.resolve('jest-transform-stub'), '^.+\\.jsx?$': require.resolve('babel-jest') }, transformIgnorePatterns: ['/node_modules/'], // support the same @ -> src alias mapping in source code moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1' }, // serializer for snapshots snapshotSerializers: [ 'jest-serializer-vue' ], testMatch: [ '**/tests/unit/**/*.spec.[jt]s?(x)', '**/__tests__/*.[jt]s?(x)' ], // https://github.com/facebook/jest/issues/6766 testURL: 'http://localhost/', watchPlugins: [ require.resolve('jest-watch-typeahead/filename'), require.resolve('jest-watch-typeahead/testname') ] }
我们挨着看下每个配置:
testEnvironment: 'jsdom':指定测试运行环境为jsdom,jsdom是一个基于Node.js的库,可以在服务器端运行JavaScript,并模拟浏览器环境,这样在测试中使用浏览器API和DOM API便不会报错。
moduleFileExtensions:指定Jest可以处理的模块文件扩展名
moduleFileExtensions: [ 'js', 'jsx', 'json', // tell Jest to handle *.vue files 'vue' ]
这里表示js,jsx,json,vue为后缀的都可以处理。
transform:指定 Jest 对测试文件进行转换的方式
transform: { // process *.vue files with vue-jest //这个规则告诉 Jest 使用 `vue-jest` 库来处理 `.vue` 文件。 //`vue-jest` 是一个 Jest 插件,可以将 `.vue` 文件转换为 JavaScript 代码, 以便 Jest 进行测试 '^.+\\.vue$': vueJest, //这个规则告诉 Jest 使用 `jest-transform-stub` 库来处理一些静态资源文件, //如 `.css`, `.png`, `.svg` 等。`jest-transform-stub` 是一个 Jest 插件, 可以将这些文件转换为一个空的模块,以便 Jest 进行测试。 '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4| webm|wav|mp3|m4a|aac|oga|avif)$':require.resolve('jest-transform-stub'), //这个规则告诉 Jest 使用 `babel-jest` 库来处理 `.js` 和 `.jsx` 文件。 //`babel-jest` 是一个 Jest 插件,可以使用 Babel 来将 ES6+ 语法转换为 ES5 语法。 '^.+\\.jsx?$': require.resolve('babel-jest') }
transformIgnorePatterns: ['/node_modules/']:指定 Jest 忽略哪些文件的转换
moduleNameMapper: {'^@/(.*)$': '<rootDir>/src/$1'}:配置模块名称的映射
snapshotSerializers: ['jest-serializer-vue']:配置在进行快照测试时使用的序列化器
testMatch:指定 Jest 应该运行哪些测试文件
testMatch: [ //这个规则告诉 Jest 匹配所有以 `.spec.js`、`.spec.jsx`、`.spec.ts` 或 `.spec.tsx` 结尾的测试文件 //并且这些文件必须在 `tests/unit` 目录或其子目录下。 '**/tests/unit/**/*.spec.[jt]s?(x)', //这个规则告诉 Jest 匹配所有以 `.test.js`、`.test.jsx`、`.test.ts` 或 `.test.tsx` 结尾的测试文件 //并且这些文件必须在 `__tests__` 目录或其子目录下 '**/__tests__/*.[jt]s?(x)' ],
testURL: 'http://localhost/':
用于指定在测试代码中使用的全局变量 window.location.href 的值。
watchPlugins:指定 Jest 在监视模式下应该使用哪些插件
watchPlugins: [ require.resolve('jest-watch-typeahead/filename'), require.resolve('jest-watch-typeahead/testname') ]
如何使用jest写测试用例
基础API
首先我们先了解几个基础的API:
describe
it
test
describe 用法
语法:describe(name, fn)
describe(name, fn) 是一个将多个相关的测试组合在一起的块。 比如,现在有一个myBeverage对象,描述了某种饮料好喝但是不酸,通过以下方式测试:
const myBeverage = { delicious: true, sour: false, }; describe('my beverage', () => { test('is delicious', () => { expect(myBeverage.delicious).toBeTruthy(); }); test('is not sour', () => { expect(myBeverage.sour).toBeFalsy(); }); });
注意:这不是强制的,你甚至可以直接把 test 块直接写在最外层。 但是如果你习惯按组编写测试,使用 describe 包裹相关测试用例更加友好。
如果你有多层级的测试,你也可以嵌套使用 describe 块:
const binaryStringToNumber = binString => { if (!/^[01]+$/.test(binString)) { throw new CustomError('Not a binary number.'); } return parseInt(binString, 2); }; describe('binaryStringToNumber', () => { describe('given an invalid binary string', () => { test('composed of non-numbers throws CustomError', () => { expect(() => binaryStringToNumber('abc')).toThrow(CustomError); }); test('with extra whitespace throws CustomError', () => { expect(() => binaryStringToNumber(' 100')).toThrow(CustomError); }); }); describe('given a valid binary string', () => { test('returns the correct number', () => { expect(binaryStringToNumber('100')).toBe(4); }); }); });
it
it() 函数是 Jest 提供的一个全局函数,用于定义一个测试用例。它接受两个参数:第一个参数是字符串,表示该测试用例的名称或描述;第二个参数是一个函数,表示该测试用例的实现代码。
function sum(a, b) { return a + b; } it('sum function', () => { expect(sum(1, 2)).toBe(3); expect(sum(-1, 1)).toBe(0); expect(sum(0.1, 0.2)).toBeCloseTo(0.3); });
test
test() 函数是 it() 函数的别名,它们的作用和用法完全一样。
断言 expect
关于expect的API,可以看官网expect的描述。
写一个最简单的测试用例
import { shallowMount } from "@vue/test-utils"; import HelloWorld from "@/components/HelloWorld.vue"; //describe声明一个套件 describe("HelloWorld.vue", () => { // 定义一个测试用例 it("renders props.msg when passed", () => { const msg = "new message"; // 渲染HelloWorld,并传入参数 const wrapper = shallowMount(HelloWorld, { props: { msg }, }); // 期望渲染wrapper返回的文本内容与msg相匹配 expect(wrapper.text()).toMatch(msg); }); });
钩子函数
beforeEach
afterEach
beforeAll
afterAll
关于这几个钩子函数的API,可以去看官网对它们的解释,这里就不再重复解释了。我们直接来看这几个钩子函数是何时触发的。 先看下列代码:
import { shallowMount } from "@vue/test-utils"; import HelloWorld from "@/components/HelloWorld.vue"; beforeEach(() => { console.log("before each"); }); afterEach(() => { console.log("after each"); }); beforeAll(() => { console.log("before All"); }); afterAll(() => { console.log("after All"); }); //describe声明一个套件 describe("HelloWorld.vue", () => { // 定义一个测试用例 it("renders props.msg when passed", () => { const msg = "new message"; // 渲染HelloWorld,并传入参数 const wrapper = shallowMount(HelloWorld, { props: { msg }, }); // 期望渲染wrapper返回的文本内容与msg相匹配 expect(wrapper.text()).toMatch(msg); }); // 定义一个测试用例 it("test add", () => { expect(1 + 1).toBe(2); }); });
打印输出结果是:
可以看出:beforeEach和afterEach是在每个测试用例执行的前后执行,而beforeAll和afterAll是在所用测试用例执行前后执行。
异步用例
如果用例中有异步代码应该怎么做呢?
Promise:测试用例返回一个Promise,则Jest会等待Promise的resove状态,如果 Promise 的状态变为 rejected, 测试将会失败。
new Promise((resolve) => { expect(wrapper.text()).toEqual('123') resolve() })
Async/Await:基于Promise的异步语法糖
如果期望Promise被Reject,则需要使用 .catch 方法。 请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则,一个fulfilled状态的Promise不会让测试用例失败。
test('the data is peanut butter', async () => { const data = await fetchData(); expect(data).toBe('peanut butter'); }); test('the fetch fails with an error', async () => { expect.assertions(1); try { await fetchData(); } catch (e) { expect(e).toMatch('error'); } });
callback:done
it('renders props.msg when passed', async (done) => { const msg = 'new message' const wrapper = shallowMount(HelloWorld as any, { props: { msg }, }) setTimeout(() => { expect(wrapper.text()).toEqual(msg) done() }, 100) })
如何使用vue-test-utils测试vue3的组件
这里,我用来我本地的组件来进行测试,直接上代码,然后对代码中的几个重要的API进行说明。 在example.spec.ts中:
// 引入@vue/test-utils中提供的两个方法 import { shallowMount, mount } from '@vue/test-utils' //引入要进行测试的组件 import SchemaForm, { NumberField } from '../../lib' // 编写组件测试的套件 describe('HelloWorld.vue', () => { // 编写测试用例 it('should render correct number field', () => { let value = 0 //渲染组件并传参 const wrapper = mount(SchemaForm, { props: { schema: { type: 'number', }, value: value, onChange: (v) => { value = v }, }, }) // 在渲染的结果里面寻找想要的子组件 const numberField = wrapper.findComponent(NumberField) //断言 expect(numberField.exists()).toBeTruthy() }) })<360>
这段代码中比较重要的几个API:
mount和shallowMount
findComponent
exists
toBeTruthy
mount和shallowMount
mount 函数会创建一个完整的组件实例,并且会渲染出组件的所有子组件。这个函数适用于测试一个组件的完整生命周期,包括子组件的交互和渲染结果。但是,由于需要渲染所有子组件,所以 mount 函数会消耗更多的资源,测试速度也会更慢。
shallowMount 函数则只会渲染当前组件,不会渲染任何子组件。这个函数适用于测试一个组件的行为,而不是它的子组件。因为不需要渲染所有子组件,所以 shallowMount 函数比 mount 函数更快。但是需要注意的是,如果组件的行为依赖于子组件的交互,那么 shallowMount 可能会测试不全面。
findComponent
用于在一个 Vue 组件的测试实例中查找子组件。
findComponent() 方法接受一个组件选项对象或组件名作为参数,并返回一个 Wrapper 实例,用于操作找到的子组件。
const numberField = wrapper.findComponent({ name: 'numberField' })
const numberFiled = wrapper.findComponent(NumberFiled)
exists
exists() 匹配器可以用于断言一个元素是否存在于 DOM 中,或者一个变量、对象、数组等是否定义或存在。
toBeTruthy
toBeTruthy() 是 Jest 测试框架中的一个匹配器(matcher),用于判断一个值是否为真(truthy)。 在 Jest 中,以下值会被视为真:
true
任何非空字符串
任何非零数值
任何非空对象
任何函数
如果一个值为上述任意一种,则 expect(value).toBeTruthy() 会返回 true,否则返回 false。
单元测试的指标
覆盖率
npm run test:unit --coverage 或者是在package.json里面将test:unit对应的命令里面添加--coverage,如:
执行之后得到:
接下来,我们挨着看一下这张表里面的字段分别代表什么意思。
File: 进行测试的文件,如schemaForm.tsx,type.ts等.
% Stmts:语句的覆盖率
% Branch:所有条件语句的测试覆盖率百分比(分支的覆盖率)。条件语句是指所有包含if、else、switch等关键字的代码块
% Funcs:函数的覆盖率,例如,如果一个JavaScript文件中包含10个函数,而测试覆盖率统计结果显示有8个函数被测试覆盖了,那么% Func覆盖率就是80%。
% Lines:行覆盖率,表示在所有代码行中,被测试覆盖的行占所有行的比例。
Uncovered Line #s:未被测试的语句有哪些
发表评论 取消回复