本文为来自 字节跳动-国际化电商-S 项目团队 成员的文章,已授权 ELab 发布。
组件作为一种被频繁复用的代码,出现线上 bug 的时候影响的是范围更广的业务,保障组件每次迭代的可靠性尤为重要。
对使用者来说,组件库的丰富的单测代表着其可靠性。
对组件开发者来说,单测的沉淀也让每次对旧代码的改动更有信心。
Jest 是 React 推荐的测试框架,也是当前众多前端组件库的单测首选。
先看一个简单的 🌰
function sum (num1, num2) {
return num1 + num2;
}
上面是一个简单的加法函数,它的功能是是输入两个数 a 和 b,期望返回它们相加的结果,它的单测写出来是这样的。
import sum from './sum.ts';
describe('util 方法测试', () => {
test("sum function test", () => {
// 对函数进行调用,期望结果是 3
expect(sum(1, 2)).toBe(3);
})
});
上面是对 sum 方法的单元测试的例子,在该测试中,对函数进行调用,传入了 1 和 2,并对返回结果的正确性进行验证。里面出现了 describe、test、expect、toBe 方法,这些是 jest 相关的基础的东西,下面逐个介绍下。
describe:描述一个模块
describe('buttonPromise 组件相关测试', () => {
// 此处编写 buttonPromise 测试用例
});
test :编写一个具体的测试用例
describe('buttonPromise 组件相关测试', () => {
test('描述测试功能:buttonpromise loading style', () => {
// 执行具体测试
})
});
expect:断言,期望某个结果符合什么样的预期
toBe:匹配器, 对某个结果进行判断是否与预期一致
test('two plus two is four', () => {
expect(sum(2, 2)).toEqual(4); // 断言结果为 4,通过测试
expect(sum(2, 2)).not.toEqual(5); // 断言结果不等于 5,通过测试
});
Jest 提供了大量的常用匹配器,针对数字、字符串、函数的调用情况、调用时的参数等
测试断言、匹配器,对测试用例结果进行判断
支持异步函数测试
// 构造一个异步函数,2s 后返回 1
const fetchData = () => new Promise((resolve) => {
setTimeout(() => {
resolve(1)
}, 2000)
})
test('支持返回一个 promise,在 resove 中进行测试', () => {
return fetchData().then(data => {
expect(data).toBe(1);
});
});
test('直接使用 .resolves / .rejects 对结果进行断言', () => {
return expect(fetchData()).resolves.toBe(1);
});
test('支持在测试用例中使用 async await', async () => {
const data = await fetchData();
expect(data).toBe(1);
});
Mock 能力
Jest 支持对一个函数、模块的 mock,有些测试场景用到了外部依赖,而这些外部依赖并不需要关心它们的执行过程,只需要它们返回一个执行后的结果,这种场景下可以使用 jest 的 mock 功能
例如下面这段代码对 axios 模块进行 mock
import axios from 'axios'
// 对整个 axios 模块进行 mock
jest.mock('axios', () => {
return {
get: jest.fn(() => Promise.resolve(1))
}
})
test('试试 mock axios', () => {
return axios.get('').then(data => expect(data).toBe(1))
})
甚至可以对引用文件内部的其他依赖做 mock
test.tsx
import axios from 'axios';
import b from './b';
export default async () => {
const data = await axios.get('./api/v1/user');
return data;
}
fn.test.tsx
import testFn from './test'
// test 文件内部使用到了 axios,在这里对其做 mock
jest.mock('axios', () => {
return {
get: jest.fn(() => Promise.resolve(1))
}
})
jest.mock('./b', () => {
return {
}
});
test('试试对引用文件内部的其他依赖做 mock', () => {
return testFn().then(data => expect(data).toBe(1))
})
jest 更多 mock 相关的知识
通过上面可以了解到 Jest 提供了单元测试常用的一些基础能力,对测试结果的断言、匹配,对外部依赖的 mock 等,但是对于组件库来说,这对于写 UI 组件单测还不够便捷,例如,
...
针对上面这些问题,业界已有了相应的工具库配合 jest 一起使用
@testing-library/react 是一个用于测试 react 组件相关的库,提供了 react 组件渲染能力、 dom 操作能力,通过它在编写 react 组件单元测试的时候可以更加关注功能测试本身,无需编写大量代码去做组件的初始化等。
@testing-library/react 提供了 render api,当开始写一个组件的单测时,可以使用它将组件渲染出来,同时其返回了一个 container 对象,是组件被渲染的容器所在,通过 container,可以进行 dom 操作,如 获取对应的节点等。
import { render } from '@testing-library/react';
import ButtonPromise from '../src';
describe('Button Promise 组件测试', () => {
test('测试 loading 功能', async () => {
// 渲染组件
const { container } = render(
<ButtonPromise id="test-copy-icon" onClick={btnClick}>
按钮
</ButtonPromise>,
);
// render 函数渲染完会返回对应的组件被渲染的容器节点,通过它可查找对应的 dom
const btn = container.querySelector('#test-copy-icon');
... 后续操作
});
上面的例子展示了如何渲染一个按钮组件,组件渲染之后,需要用户去点击,这涉及到用户行为的模拟,针对这一类场景,对应的解决方案是 @testing-library/user-event
@testing-library/user-event 是一个模拟用户行为的库,包括对用户鼠标行为(单击、双击、鼠标 hover 等)、键盘行为的模拟。
继续上面的例子,在按钮成功渲染出来后,对其进行点击
import { render } from '@testing-library/react';
import ButtonPromise from '../src';
describe('Button Promise 组件测试', () => {
test('测试 loading 功能', async () => {
// userEvent 初始化
const user = userEvent.setup();
const { container } = render(
<ButtonPromise id="test-copy-icon" onClick={btnClick}>
按钮
</ButtonPromise>,
);
// render 函数渲染完会返回对应的组件被渲染的容器节点,通过它可查找对应的 dom
const btn = container.querySelector('#test-copy-icon');
// 点击按钮
user.click(btn);
});
用户的行为通常伴随着数据的变化、界面的变化,这些变化是检查组件经过用户交互后是否正确的判断标准。数据的变化通过 jest 提供的各类匹配器(toEqual、toMatch 等)可以进行判断,界面的变化则需要通过对 dom 状态的检验。
继续上面的例子,上面的部分已经做到了对 buttonPromise 组件的渲染,点击,而该组件的功能,是期望点击后用户可以看到 loading 效果.
import { render } from '@testing-library/react';
import ButtonPromise from '../src';
describe('Button Promise 组件测试', () => {
test('测试 loading 功能', async () => {
// 根据组件功能,传入 onClick 的函数返回一个 promise
// 在 resolve 之前,按钮会有 loading 效果
const btnClick = () => new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 2000);
})
// userEvent 初始化
const user = userEvent.setup();
const { container } = render(
<ButtonPromise id="test-copy-icon" onClick={btnClick}>
按钮
</ButtonPromise>,
);
// render 函数渲染完会返回对应的组件被渲染的容器节点,通过它可查找对应的 dom
const btn = container.querySelector('#test-copy-icon');
// 点击按钮
user.click(btn);
await waitFor(() => {
// 期望按钮上有 loading 样式
expect(btn).toHaveClass('arco-btn-loading')
});
});
通过上面 jest 与 test-library 相关的基础介绍,到最后完成了 button-promise 组件单元测试的编写。
明确测试目的(测什么功能,什么结果是正确的)
每个 test 只包含一个测试目的
多站在用户的视角进行考虑(把自己当作在使用组件的用户,做出某些行为后应该看到什么)
由于 arcoS 是对 arco 的封装,对于一些组件的基础功能可以不进行测试
以上面组件库中 button-promise 组件为例
功能 a:onClick 绑定一个返回了 promise 的函数,组件将进入 loading 状态,直到 promise 进入 resolve 状态)
通过脚手架创建完组件后,会产生对应的文件夹 __tests__
,以及对应的单元测试文件 xxx.test.tsx,文件内容如下
import { render } from '@testing-library/react';
import xxx from '../src';
describe('组件名', () => {
test('测试功能1', () => {
// 在这里写单元测试
});
test('测试功能2', () => {
// 在这里写单元测试
});
编写完测试用例后,执行 yarn test (命令已在创建组件时被初始化),可以看到测试结果
成功的测试结果
失败的结果,可以看到测试结果期望有 arco-btn-loading1 的类名,但是实际 received 到的类并没有
确定测试功能
import ButtonGroup from './src';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
describe('Button-Group Test', () => {
// 不同功能验证编写不同用例
test('droplist show correct', async () => {
const user = userEvent.setup();
// 渲染按钮
const { container, getByText } = render(
<ButtonGroup
title={'更多'}
dropList={[{
title: '批量导出',
}, {
render: () => <span>操作记录</span>,
}]}
/>
);
// 模拟用户行为
user.click(getByText('更多'))
await waitFor(() => {
// 期望传入的两个按钮在点击更多后正常渲染
expect(getByText('批量导出')).toBeInTheDocument()
expect(getByText('操作记录')).toBeInTheDocument()
});
})
// 不同功能验证编写不同用例
test('droplist click correct', async () => {
const user = userEvent.setup({
pointerEventsCheck: 0
});
const exportFn = jest.fn(() => {});
const { getByText } = render(
<ButtonGroup
title={'更多'}
dropList={[{
title: '批量导出',
onClick: () => exportFn()
}]}
/>
);
user.click(getByText('更多'))
await waitFor(() => {
const exportBtn = getByText('批量导出');
user.click(exportBtn);
// 期望 exportFn 函数在点击批量导出后能够被调用一次
expect(exportFn).toBeCalledTimes(1)
});
})
})
jest 官方文档
React Test Library 工具库
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。
欢迎关注公众号 ELab团队 收货大厂一手好文章~
- END -使用下方内推码或内推链接,可投递 字节跳动-国际化电商-S 项目团队 相关岗位
内推码:WWCM1TA 内推链接:https://job.toutiao.com/s/rj1fwQW