最近在写业务代码测试时候,对如何写规范的测试产生了兴趣,下面是一点学习心得。
React组件测试
React组件的测试,选择的测试工具一般是官方测试工具库和Enzyme。
React组件有两种存在方式:虚拟DOM对象和真是DOM节点。针对这两种形式,官方测试工具库对这两种方式,有不同测试API。
shallow rendering: 测试虚拟DOM
DOM rendering: 测试真实DOM
Enzyme库shallow,render和mount方法
Enzyme 是 Airbnb 开源的专为 React 服务的测试框架。
shallow方法是对官方测试工具库Shallow Rendering的封装。Shallow Rendering是将一个组件渲染成虚拟DOM,并且只渲染第一层,不渲染子组件,所以渲染速度很快。优点:渲染速度快。
render方法将组件解析为一段静态HTML字符串。
mount方法将组件完全解析成真实DOM结构。
为什么要写单元测试
保障质量,方便重构,避免需求增加后,代码腐化。
什么是好的单元测试
测试一般遵循give-when-then 的结构,这样写出的测试代码结构清晰,易于理解。
比如
// production code
const computeTotalAmount = (products) => {
return products.reduce((total, product) => total + product.price, 0);
}
// testing code
it('should return summed up total amount 1000 when there are three products priced 200, 300, 500', () => {
// given - 准备数据
const products = [
{ name: 'nike', price: 200 },
{ name: 'adidas', price: 300 },
{ name: 'lining', price: 500 },
]
// when - 调用被测函数
const result = computeTotalAmount(products)
// then - 断言结果
expect(result).toBe(1000)
})
好的测试的几条原则
只关注输入输出,不关注内部实现
简单来说,就是只要测试输入不变,输出也不变。这是重构的基础。因为重构的定义是在不改变软件外部可观测行为的基础上,调整软件内部的实现。
对于去mock外部依赖,其实也是关注内部实现。因为mock失败了,测试也会挂,但其实并不是真实业务逻辑出问题了,建议少用mock。只测试一条分支
简单来说,就是只测试一个业务场景,对应在任务拆解中一个细粒度的task,这样做的好处是轻量,写起来快,可以给测试一个很详细的描述。表达力极强
- 测试描述,当遵循只测试一条分支原则时,一般测试描述就能给出具体的业务上下文,这样看测试的人,通过测试就能获取一些业务的导入。
- 测试数据,测试数据不应该包含与本次测试无关的数据,只准备满足所测场景的最小数据。
- 清晰的断言,测试失败时候,能给出期望的数据与实际数据具体差异。
测试不包含逻辑
一般模式都是:准备数据->调用函数->断言。如果包含了逻辑,一是增加了阅读负担,而是测试挂了,不知道是实现挂了,还是测试本身挂了。运行速度快
只有单元测试运行快,他才是快速获取反馈的一种手段。
如何才能尽量让测试运行更快,有一些策略:
1.把耗时的操作能不能挪到更高层级的测试中,比如与第三方系统集成。
2.尽可能避免依赖。
React component测试
React component的拆分应遵循单一职责,分为以下几类:
- 通用UI组件
- 功能组件
- 展示型业务组件
- 容器型业务组件
功能型组件,指的是跟业务无关的另一类组件,它是功能型的,更像是底层支撑着业务组件运作的基础组件,比如路由组件、分页组件等。这些组件一般逻辑多一点,UI 部分少一些。
组件的测试一般遵循 shallow -> find(Component) -> 断言的三段式
那React component应该测什么,不该测什么?
- 分支渲染逻辑必须侧
- 事件调用和参数传递必须侧
- 渲染出来的UI一般不放在单元测试
因为分支渲染和事件调用,有业务价值和文档作用。添加测试能方便重构,也能做文档参考。
单元测试一般不测纯UI,因为去获取一些DOM节点,不好加断言,写测试成本较高。UI测试一般由自动化测试监控,比如可以用backstopjs
去做image compare
举个例子
- 业务组件-分支渲染测试
export const CommentsSection = ({ comments }) => (
{comments.length > 0 && (
Comments
)}
{comments.map((comment) => (
)}
)
第一个测试用例是,如果没有comment,则不渲染comment header和comment内容
import { CommentsSection } from './index'
import { Comment } from './Comment'
test('should not render a header and any comment sections when there is no comments', () => {
const component = shallow( )
const header = component.find('h2')
const comments = component.find(Comment)
expect(header).toHaveLength(0)
expect(comments).toHaveLength(0)
})
第二个测试用例,如果有comment,则正确渲染comment header和comment内容
test('should render a comments section and a header when there are comments', () => {
const contents = [
{ id: 1, author: 'test user 1', comment: 'test comment 1' },
{ id: 2, author: 'test user 2', comment: 'test comment 1' },
]
const component = shallow( )
const header = component.find('h2')
const comments = component.find(Comment)
expect(header.html()).toBe('Comments')
expect(comments).toHaveLength(2)
})
2.业务组件-事件调用
测试场景是:当某条产品被点击时,应该将产品相关的信息发送给埋点系统进行埋点。
export const ProductItem = ({
id,
productName,
introduction,
trackPressEvent,
}) => (
trackPressEvent(id, productName)}>
)
测试内容:模拟产品点击事件,相关函数被调用,并能传递正确的参数。
import { ProductItem } from './index'
test(`
should send product id and name to analytics system
when user press the product item
`, () => {
const trackPressEvent = jest.fn()
const component = shallow(
)
component.find(TouchableWithoutFeedback).simulate('press')
expect(trackPressEvent).toHaveBeenCalledWith(
100832,
'iMac Pro - Power to the pro.'
)
})
可以发现,这些测试还是依赖了一些组件内部实现,比如find TouchableWithoutFeedback组件,这些无法避免。因为组件本质是渲染树,要获取点击节点,必须通过组件名,className等选择器。测试时尽量少暴露组件细节。
对纯函数可以用参数化测试
纯函数就是一个输入对应固定的输出,没有任何外部依赖。
比如
test.each([
['abc@hotmail.com', true, 'should return true given correct toEmail format'],
['abc@hotmail.com,um_agt@outlook.com', true, 'should return true given correct toEmail format'],
['abc,abc@hotmail.com', false, 'should return false given wrong toEmail format'],
['abc@hotmail.com,abc@hotmail.,um_agt@outlook.com', false, 'should return false given wrong toEmail format'],
])(
'should return correct boolean value given email address',
(expected, input, description) => {
expect(isCorrectEmailFormat(input)).toEqual(expected)
}
)
参数化测试方便准备数据,测试用例在一个地方,清晰易读。