引言
在2020的今天,构建一个 web 应用对于我们来说,并非什么难事。因为有很多足够多优秀的的前端框架(比如 React
,Vue
和 Angular
);以及一些易用且强大的UI库(比如 Ant Design
)为我们保驾护航,极大地缩短了应用构建的周期。
但是,互联网时代也急剧地改变了许多软件设计,开发和发布的方式。开发者面临的问题是,需求越来越多,应用越来越复杂,时不时会有一种失控的的感觉,并在心中大喊一句:“我太南了!”。严重的时候甚至会出现我改了一行代码,却不清楚其影响范围情况。这种时候,就需要测试的方式,来保障我们应用的质量和稳定性了。
接下来,让我们学习下,如何给 React
应用写单元测试吧
需要什么样的测试
软件测试是有级别的,下面是《Google软件测试之道》一书中,对于测试认证级别的定义,摘录如下:
-
级别1
- 使用测试覆盖率工具。
- 使用持续集成。
- 测试分级为小型、中型、大型。
- 创建冒烟测试集合(主流程测试用例)。
- 标记哪些测试是非确定性的测试(测试结果不唯一)。
-
级别2
- 如果有测试运行结果为红色(失败)就不会发布。
- 每次代码提交之前都要求通过冒烟测试。(自测,简单走下主流程)
- 各种类型的整体代码覆盖率要大于50%。
- 小型测试的覆盖率要大于10%。
-
级别3
- 所有重要的代码变更都要经过测试。
- 小型测试的覆盖率大于50%。
- 新增重要功能都要通过集成测试的验证。
-
级别4
- 在提交任何新代码之前都会自动运行冒烟测试。
- 冒烟测试必须在30分钟内运行完毕。
- 没有不确定性的测试。
- 总体测试覆盖率应该不小于40%。
- 小型测试的代码覆盖率应该不小于25%。
- 所有重要的功能都应该被集成测试验证到。
-
级别5
- 对每一个重要的缺陷修复都要增加一个测试用例与之对应。
- 积极使用可用的代码分析工具。
- 总体测试覆盖率不低于60%。
- 小型测试代码覆盖率应该不小于40%。
小型测试,通常也叫单元测试,一般来说都是自动化实现的。用于验证一个单独的函数,组件,独立功能模块是否可以按照预期的方式运行。
而对于开发者来说,重要的是进行了测试的动作。本篇文章主要围绕着React组件单元测试展开的,其目的是为了让开发人员可以站在使用者的角度考虑问题。通过测试的手段,确保组件的每一个功能都可以正常的运行,关注质量,而不是让用户来帮你测试。
在编写单元测试的时候,一定会对之前的代码反复进行调整,虽然过程比较痛苦,可组件的质量,也在一点一点的提高。
技术栈选择
当我们想要为 React
应用编写单元测试的时候,官方推荐是使用 React Testing Library + Jest 的方式。Enzyme 也是十分出色的单元测试库,我们应该选择哪种测试工具呢?
下面让我们看一个简单的计数器的例子,以及两个相应的测试:第一个是使用 Enzyme 编写的,第二个是使用 React Testing Library 编写的。
counter.js
// counter.js
import React from "react";
class Counter extends React.Component {
state = { count: 0 };
increment = () => this.setState(({ count }) => ({ count: count + 1 }));
decrement = () => this.setState(({ count }) => ({ count: count - 1 }));
render() {
return (
<div>
<button onClick={this.decrement}>-</button>
<p>{this.state.count}</p>
<button onClick={this.increment}>+</button>
</div>
);
}
}
export default Counter;
counter-enzyme.test.js
// counter-enzyme.test.js
import React from "react";
import { shallow } from "enzyme";
import Counter from "./counter";
describe("<Counter />", () => {
it("properly increments and decrements the counter", () => {
const wrapper = shallow(<Counter />);
expect(wrapper.state("count")).toBe(0);
wrapper.instance().increment();
expect(wrapper.state("count")).toBe(1);
wrapper.instance().decrement();
expect(wrapper.state("count")).toBe(0);
});
});
counter-rtl.test.js
// counter-rtl.test.js
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import Counter from "./counter";
describe("<Counter />", () => {
it("properly increments and decrements the counter", () => {
const { getByText } = render(<Counter />);
const counter = getByText("0");
const incrementButton = getByText("+");
const decrementButton = getByText("-");
fireEvent.click(incrementButton);
expect(counter.textContent).toEqual("1");
fireEvent.click(decrementButton);
expect(counter.textContent).toEqual("0");
});
});
比较两个例子,你能看出哪个测试文件是最好的嘛?如果你不是很熟悉单元测试,可能会任务两种都很好。但是实际上 Enzyme
的实现有两个误报的风险:
- 即使代码损坏,测试也会通过。
- 即使代码正确,测试也会失败。
让我们来举例说明这两点。假设您希望重构组件,因为您希望能够设置任何count值。因此,您可以删除递增和递减方法,然后添加一个新的setCount方法。假设你忘记将这个新方法连接到不同的按钮:
counter.js
// counter.js
export default class Counter extends React.Component {
state = { count: 0 };
setCount = count => this.setState({ count });
render() {
return (
<div>
<button onClick={this.decrement}>-</button>
<p>{this.state.count}</p>
<button onClick={this.increment}>+</button>
</div>
);
}
}
第一个测试(Enzyme
)将通过,但第二个测试(RTL
)将失败。实际上,第一个并不关心按钮是否正确地连接到方法。它只查看实现本身,也就是说,您的递增和递减方法执行之后,应用的状态是否正确。
这就是代码损坏,测试也会通过。
现在是2020年,你也许听说过 React Hooks
,并且打算使用 React Hooks
来改写我们的计数器代码:
counter.js
// counter.js
import React, { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count => count + 1);
const decrement = () => setCount(count => count - 1);
return (
<div>
<button onClick={decrement}>-</button>
<p>{count}</p>
<button onClick={increment}>+</button>
</div>
);
}
这一次,即使您的计数器仍然工作,第一个测试也将被打破。Enzyme
会报错,函数组件中无法使用state
:
ShallowWrapper::state() can only be called on class components
接下来,就需要改写单元测试文件了: