前端项目中写单元测试其实很简单
共 13763字,需浏览 28分钟
·
2024-05-24 08:50
大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
本文转载于UCloud云通信技术团队
一、关于自动化测试
1、测试分类
自动化测试类型常分为以下三种,各有优缺点:
-
单元测试(Unit Test)
-
对项目中低耦合的工具类库和公共子组件进行测试,较为简单,能在一定程度上保障代码质量 -
集成测试(Integration Test)
-
对于耦合度较高的函数/组件对外暴露的接口进行测试,能较大程度保障产品质量,但开发成本高 -
UI测试(UI Test)
-
前端中UI变动大,适合人工检查
了解测试术语
-
1、TDD(测试驱动开发) -
2、BDD(行为驱动开发) -
3、测试覆盖率 -
4、快照测试 -
5、模拟函数 -
6、断言
2、单元测试框架
-
Jest[1]:是一个广受欢迎的单元测试框架,简单易用,功能强大。 -
Vitest[2]:它由 Vue / Vite 团队成员开发和维护,在 Vite 的项目集成它会非常简单,而且速度非常快。 -
Mocha:一个灵活的测试框架,需要各种插件来配合使用。 -
Karma:能在真实的浏览器中测试,可配置其他单元测试框架 -
Jasmine:功能全面的测试框架,相对复杂、不够灵活
测试框架太多,且各有优势,大多数写法相差不多。
我们这里选择Jest
来分享,其他测试框架可以自行了解。
3、单元测试适用的测试对象有哪些?
-
1、常见工具类函数 -
2、公共子组件 -
3、接口请求数据
二、给项目配置Jest
1、安装
yarn add -D jest
或npm install jest -D
2、配置(非必需)
如果你想获得更多的jest配置,可以增加配置文件。
比如项目中的Jest,默认不显示测试覆盖率和测试报告等,想要支持,就需要我们将Jest
的配置文件暴露出来,只需要执行yarn test --init
或npx jest --init
然后根据需求选择对应的配置,最后会在根目录下生成jest.config.js
文件
根据提示选择即可,这里我们选择JsDom
环境,需要代码测试覆盖率报告,自动清除每个单元测试之间的模拟调用和实例。
执行完成后,发现在项目根目录下多了一个jest.config.js
文件,里面包含了各种配置说明
module.exports = {
// 是否显示覆盖率报告
collectCoverage: true,
// 告诉 jest 文件测试要求的阈值,单位为百分比
// coverageThreshold: {
// global: {
// statements: 90, // 每行
// functions: 80, // 每个函数
// branches: 90 // 分支覆盖率
// }
// }
}
此时再次执行单元测试,发现显示了测试覆盖率
用浏览器打开coverage
目录下的index.html
,可以看到此时页面显示了测试报告
3、配置快速执行命令
package.json
{
"scripts": {
"start": "node index.js",
"test": "jest",
"coverage": "jest --coverage"
}
}
执行命令启动单元测试yarn test
或yarn coverage
4、项目配置
一般通过脚手架生成的项目,已经默认配置了测试框架,比如React的项目配置了
Jest
,Vue3.x
项目默认配置了Vitest
。
-
Jest默认支持Commonjs
-
如果你的项目不支持ESM,需要安装 @babel/core
,@babel/preset-env
进行转译。 -
如果你的项目需要支持TS,可以 @types/jest
、@babel/preset-typescript
执行yarn test
发现报错,是因为需要配置babel
配置babel
安装插件,在根目录下新建
.babelrc
文件
// .babelrc
{
"plugins": [
[
"@babel/plugin-syntax-jsx"
]
],
"presets": [ "@babel/preset-env", "@babel/preset-react" ]
}
注意
-
如何支持或忽略.css文件 -
如何忽略单行、函数或文件、目录
5、快速上手单元测试
比如我们有sum.js
export function sum(a, b){
return a + b;
}
export function mins(a, b){
return a - b;
}
为这个函数写测试文件
import { sum, mins } from './sum'
it(`should add 1 + 2 to equal 3`, () => {
expect(sum(1, 2)).toBe(3);
});
test(`mins 2 - 1 to equal 1`, () => {
expect(min(2, 1)).toBe(1);
})
入门很简单,只需要针对每个函数做一些预期的校验即可,当不小心改动了源代码导致输出的结果和预期不符,将会测试不通过,这样就保证了代码功能的稳定。
describe、test预留字段基本没有区别,描述方式不同,一个it should,另一个test action
三、项目中如何开始写单元测试
写单元测试要考虑清楚几点:
-
测试的主要目的不是证明代码的正确,而是为了发现错误。 -
测试代码,只考虑外部接口,不考虑内部实现 -
充分考虑数据的边界条件 -
对重点、核心代码重点测试 -
减少测试代码数量,避免无用功 -
基于需求写单元测试
1、在项目根目录下新建tests目录,将单元测试文件放在其中,测试文件命名
xx.test.js
,优点是可以更好的管理测试文件,缺点是不好找到源文件2、在对应文件的目录下新建
__test__
目录,测试文件放置其中,优点就是容易找到执行文件,但不容易过滤和管理
1、给工具函数写单元测试
给工具函数写测试函数是单元测试很重要的一个场景,我们以金额千分位格式化处理函数为例,通过单元测试发现问题。
// 将数字千分位格式化后返回对应的字符串
export function getThousandFormatNum(num) {
const str = num + '';
const reg = str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+.)/g : /(\d{1,3})(?=(\d{3})+(?:$|.))/g;
return str.replace(reg, '$1,');
}
单元测试
import { getThousandFormatNum } from './common'
describe('getThousandFormatNum', () => {
// 常规数字格式化
it('should return a string with thousand format', () => {
expect(getThousandFormatNum(1000)).toBe('1,000');
expect(getThousandFormatNum(1000000)).toBe('1,000,000');
expect(getThousandFormatNum(123456789)).toBe('123,456,789');
});
// 格式化后和本身相同的数字
it('should return the same number if it is not greater than 999', () => {
expect(getThousandFormatNum(0)).toBe('0');
expect(getThousandFormatNum(999)).toBe('999');
});
// 格式化负数
it('should handle negative numbers correctly', () => {
expect(getThousandFormatNum(-1000)).toBe('-1,000');
expect(getThousandFormatNum(-1000000)).toBe('-1,000,000');
expect(getThousandFormatNum(-123456789)).toBe('-123,456,789');
});
// 格式化带小数的数字
it('should handle decimal numbers correctly', () => {
expect(getThousandFormatNum(1234.56)).toBe('1,234.56');
expect(getThousandFormatNum(1234567.89)).toBe('1,234,567.89');
});
});
执行单元测试
2、给组件写单元测试(快照测试)
前端主要就是组件,但业务组件变动比较频繁,所以倾向于给公共组件或组件库增加单元测试,防止组件扩展或变更导致业务Bug。
我们以APP.js组件为例,写单元测试,并生成快照。
function App() {
return (
<div className="App">
<HashRouter basename="/">
<div style={{marginBottom: 20}}>
<Link style={{marginRight: 20}} to="/">Home更新版本1</Link>
<Link to="/about">About更新版本123</Link>
</div>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<AFunction />}></Route>
<Route path="/about" element={<BFunction />} />
</Routes>
</Suspense>
</HashRouter>
</div>
);
}
export default App;
给App.js
写单元测试
import { render, screen, act } from '@testing-library/react';
import App from './App';
test('renders learn react link', async () => {
let tree;
await act(async () => {
tree = render(<App />);
})
const linkElement = screen.getByText(/About更新版本123/i);
expect(linkElement).toBeInTheDocument();
expect(tree).toMatchSnapshot();
});
当我们改动App.js
,单元测试发现上个版本的快照更新了,就会报错,提醒检查,如果更改没问题,可以执行u
更新快照
3、模拟函数(Mock)
Mock是单元测试中很重要的一部分,他一般在下面场景中使用
-
模拟数据 -
模拟接口请求 -
模拟定时器,比如setTimout 1小时,那每次测试花费一小时就疯了 -
组件使用Redux怎么测试
在组件中,经常有一些引用的变量
const mock = jest.fn();
mock.mockReturnValue(42);
mock(); // 42
mock.mockReturnValue(43);
mock(); // 43
模拟接口请求
test('async test', async () => {
const asyncMock = jest.fn().mockResolvedValue(43); // Promise
await asyncMock(); // 43
});
模拟函数有很多,在实际使用过程中需要各种结合使用,这里仅展示了最简单的使用。
4、常用断言方法
在工具函数测试过程中,我们常常要判断变量类型和值,测试框架往往提供了判断方法,下面是Jest一些常见的判断,更多可以查阅官网[3]
toBe:判断测试结果为某个值
not:否定判断
test('the best flavor is not coconut', () => {
expect(bestLaCroixFlavor()).toBe('coconut');
});
test('the best flavor is not coconut', () => {
expect(bestLaCroixFlavor()).not.toBe('coconut');
});
toEqual:检测引用类型,递归检查属性和属性值
toEqual会调用Object.is方法,toBe ===
const can1 = {
flavor: 'grapefruit',
ounces: 12,
};
const can2 = {
flavor: 'grapefruit',
ounces: 12,
};
describe('the La Croix cans on my desk', () => {
test('have all the same properties', () => {
expect(can1).toEqual(can2); // true
});
test('are not the exact same can', () => {
expect(can1).not.toBe(can2); // true
});
});
toMatch:匹配字符串规则,正则匹配
describe('an essay on the best flavor', () => {
test('mentions grapefruit', () => {
expect(essayOnTheBestFlavor()).toMatch(/grapefruit/);
expect(essayOnTheBestFlavor()).toMatch(new RegExp('grapefruit'));
});
});
toBeTruthy:匹配if条件为真
drinkSomeLaCroix();
if (thirstInfo()) {
drinkMoreLaCroix();
}
四、查看单元测试的结果
单元测试完成后,执行测试命令
yarn test
或 npx jest
1、测试覆盖率解读
-
Stmts (Statements):语句覆盖率,即被测试覆盖的代码语句的百分比。在你的代码中,92.85% 的语句被测试覆盖。 -
Branch:分支覆盖率,即被测试覆盖的条件分支的百分比。在你的代码中,100% 的分支被测试覆盖。 -
Funcs (Functions):函数覆盖率,即被测试覆盖的函数的百分比。在你的代码中,83.33% 的函数被测试覆盖。 -
Lines:行覆盖率,即被测试覆盖的代码行数的百分比。在你的代码中,100% 的行被测试覆盖。 -
Uncovered Line:未覆盖的行号。这一列列出了未被测试覆盖的代码行的行号范围。
2、测试信息解读
-
Test Suites: 2 passed, 2 total:这表示你有 2 个测试套件,其中所有的 2 个测试套件都通过了。 -
Tests: 8 passed, 8 total:这表示你一共运行了 8 个测试,其中所有的 8 个测试都通过了。 -
Snapshots: 1 total:这表示1个快照测试(Snapshot Testing)。 -
Time: 2.703 s:这表示测试运行的时间为 2.703 s 秒。
3、参考
-
Jest官网[4] -
Jest实践指南[5]
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一下