前端单测想必这个大多数人都听过,但是却被大多数人无视。写单元测试真的是一件费时费力,效果又不太显著的事,甚至在短期内根本无法获得正向反馈。即使在无数个深夜为维护别人的代码而焦头烂额时,也没有想过要为我们自己的组件写一写单测。
关于单测的好处,相信网上已经给出了各种各样的答案,或许你自己内心也有已经有自己的答案,本篇文章就不过多赘述。本篇文章将从以下两个重点来聊聊前端组件单测。
- 如何写好前端组件的单元测试。
- 常见示例
1 如何写好前端组件的单元测试
1.1 选择合适的测试框架
俗话说:“千里之行,始于足下”。要写好单元测试,一定要选择合适的测试框架,合适的测试框架才能让你写出好的单元测试,否则不仅不能让我们的代码变得更好维护,反而会成为我们的负担,最后我们还需要消耗更多头发去维护单元测试,这不没事找事呢嘛。我们今天着重讲react组件的单元测试,直接上结论: jest + React Testing Library(RTL) 你值得拥有。
1.2 了解测试重点(金字塔测试模型)
上面是金字塔测试模型,整个测试分为三层:
- UI层(User Interface用户界面)层:在用户界面上进行操作完成测试——黑盒测试/功能测试
- Server层:服务层测试,主要在集成测试阶段,测试模块间的调用关系(一个模块给另一个模块提供调用,就说提供服务)——主要测试代码之间的调用关系,也是接口测试的核心
- Unit层:单元层,主要在单元测试阶段,使用白盒和黑盒的方法测试某个模块的功能是否正确
从金字塔模型中我们可以看出,UI层的测试效率低,发现问题的能力也较弱,投入产出比较低,而Server层和Unit层测试的投入产出比相对较高,其中我们应该重点测试Unit层。
1.3 测试误区 -- 过度测试实现细节
很多人在做组件单元测试的时候会去测试组件的实现细节,其实这么做反而事倍功半,还容易陷入为了实现各个细节的测试而对组件进行频繁改动的窘境,其实很多人这么做主要的目的也是在于过度追求组件代码覆盖率。在测试组件时我们有两种视角,一是组件的使用者,二是用户视角。作为组件的开发者,我们应该相信组件的使用者,更多站在用户视角上对组件进行测试。因为组件都具有一个特性
对于相同的输入,有相同的输出,即只要给组件同样的属性。
基于组件的这一特性,我们的测试重点可以归纳为:交互逻辑,输入输出验证的测试。我们可以大胆忽略组件内部的实现细节,仅针对组件的输入输出与交互逻辑进行验证,实际上输入输出的验证就是组件使用者视角,交互逻辑的验证就是用户视角。逻辑的实现与开发者本身的经验和素质是有直接关系的。而上文提到的为了测试组件而频繁改动组件的行为,题主认为测试逻辑在组件开发的早期就应该考虑,而不应该在测试阶段再去考虑。
2 常见示例
这里我们以一个常见的循环滚动的公告栏组件做样例,具体的代码可以点此查看 该组件具备以下能力:
- 循环播放
- 鼠标悬浮暂停滚动
2.1 断言
const linkMessage = '公告位1 <a target="_blank" href="https://www.baidu.com">返回旧版</a>';
const normalMessage = '公告位2';
const dataSource = [linkMessage, normalMessage];
const speed = 5000;
describe('slide-notice', () => {
const ele = (props: Partial<SlideNoticeProps> = {}) => {
return <SlideNotice dataSource={dataSource} {...props} />;
};
// 组件渲染测试
test('empty dataSource', () => {
render(ele({ dataSource: [] }));
const dom = querySelector('.bp-slide-notice');
expect(dom).toBeNull();
});
})
通过一个最基础的测试样例我们可以了解到组件的断言,更多断言API可以点此了解
2.2 模拟组件定时器
// 测试是否循环轮播
test('loop', () => {
jest.useFakeTimers();
render(ele({ speed }));
const initActiveDom = querySelector('.bp-slide-notice-message.active');
// 第一次执行定时器
act(() => {
jest.runOnlyPendingTimers();
});
const firstSlideDom = querySelector('.bp-slide-notice-message.active');
// 第二次执行定时器,完成dataSource数据的循环
act(() => {
jest.runOnlyPendingTimers();
});
const secondSlideDom = querySelector('.bp-slide-notice-message.active');
expect(initActiveDom.innerHTML).toBe(linkMessage);
expect(firstSlideDom.innerHTML).toBe(normalMessage);
expect(secondSlideDom.innerHTML).toBe(linkMessage);
});
// 测试鼠标移入是否暂停
test('pause when mouseEnter', () => {
jest.useFakeTimers();
render(ele({ speed }));
const initActiveDom = querySelector('.bp-slide-notice-message.active');
// 鼠标移入
fireEvent.mouseEnter(initActiveDom);
// 第一次执行定时器
act(() => {
jest.runOnlyPendingTimers();
});
const firstSlideDom = querySelector('.bp-slide-notice-message.active');
expect(initActiveDom.innerHTML).toBe(linkMessage);
expect(firstSlideDom.innerHTML).toBe(linkMessage);
});
常见示例会持续更新...