前言
单元测试是现代软件开发中不可或缺的一部分。它可以让开发人员更加自信地修改和重构代码,而不会破坏现有的功能。在编写组件时,单元测试可以帮助我们确保组件的正确性和可靠性,并提高代码质量。此外,单元测试还可以帮助我们快速定位和修复代码中的潜在问题和错误,从而提高回归效率,减少调试和修复错误所需的时间和成本。
在Vue3中,我们可以使用一些现成的单元测试框架和工具,如Jest和Vue Test Utils。在写单元测试时,我们可以模拟各种不同的场景和输入,以测试组件在不同情况下的行为和反应。
今天,我们便来了解下如何在Vue3中进行单元测试。
准备工作
- 创建一个vue3项目:
vue create vue-test - 选择手动选择功能:
3. 勾上
unit Testing(单元测试)
4. 选好之后回车,选择
Vue版本,这里选择3.x:
5. 选择一个
格式化标准,分别是:
只提示错误的ESlintESlint+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配置内容放的位置:
In dedicated config files:单独的config文件In package.json:放在package,json中
我选择的是单独的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:
describeittest
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);
});
});
钩子函数
beforeEachafterEachbeforeAllafterAll
关于这几个钩子函数的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()
})
})
这段代码中比较重要的几个API:
mount和shallowMountfindComponentexiststoBeTruthy
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:未被测试的语句有哪些