测试驱动的 React.js 开发:使用 Enzyme 和 Jest 进行 React.js 单元测试
已发表: 2022-03-11根据 Michael Feathers 的说法,任何没有测试的代码都被称为遗留代码。 因此,避免创建遗留代码的最佳方法之一是使用测试驱动开发 (TDD)。
虽然有许多工具可用于 JavaScript 和 React.js 单元测试,但在这篇文章中,我们将使用 Jest 和 Enzyme 使用 TDD 创建一个具有基本功能的 React.js 组件。
为什么使用 TDD 创建 React.js 组件?
TDD 为您的代码带来了许多好处——高测试覆盖率的优势之一是它可以轻松地重构代码,同时保持代码的清洁和功能。
如果您之前创建过 React.js 组件,您就会意识到代码可以增长得非常快。 它充满了由与状态更改和服务调用相关的语句引起的许多复杂条件。
每个缺少单元测试的组件都有难以维护的遗留代码。 我们可以在创建生产代码后添加单元测试。 但是,我们可能会冒着忽略一些本应经过测试的场景的风险。 通过首先创建测试,我们有更高的机会覆盖组件中的每个逻辑场景,这将使其易于重构和维护。
我们如何对 React.js 组件进行单元测试?
我们可以使用许多策略来测试 React.js 组件:
- 我们可以验证当某个事件被调度时,
props
中的特定函数被调用了。 - 我们还可以在给定当前组件状态的情况下获取
render
函数的结果,并将其与预定义的布局相匹配。 - 我们甚至可以检查组件的子组件数量是否与预期数量相匹配。
为了使用这些策略,我们将使用两个在 React.js 中进行测试时派上用场的工具:Jest 和 Enzyme。
使用 Jest 创建单元测试
Jest 是 Facebook 创建的一个开源测试框架,与 React.js 有很好的集成。 它包括一个用于测试执行的命令行工具,类似于 Jasmine 和 Mocha 提供的工具。 它还允许我们创建几乎为零配置的模拟函数,并提供一组非常好的匹配器,使断言更易于阅读。
此外,它还提供了一个非常好的功能,称为“快照测试”,可以帮助我们检查和验证组件渲染结果。 我们将使用快照测试来捕获组件的树并将其保存到一个文件中,我们可以使用该文件将其与渲染树(或作为第一个参数传递给expect
函数的任何内容)进行比较。
使用 Enzyme 挂载 React.js 组件
Enzyme 提供了一种机制来挂载和遍历 React.js 组件树。 这将帮助我们访问它自己的属性和状态以及它的子道具,以便运行我们的断言。
Enzyme 为组件安装提供了两个基本功能: shallow
和mount
。 shallow
函数仅在内存中加载根组件,而mount
加载完整的 DOM 树。
我们将结合 Enzyme 和 Jest 来挂载 React.js 组件并在其上运行断言。
设置我们的环境
您可以查看这个 repo,它具有运行此示例的基本配置。
我们使用以下版本:
{ "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }
使用 TDD 创建 React.js 组件
第一步是创建一个失败的测试,它将尝试使用酶的浅层函数渲染 React.js 组件。
// MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });
运行测试后,我们得到以下错误:
ReferenceError: MyComponent is not defined.
然后我们创建提供基本语法的组件以使测试通过。
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }
在下一步中,我们将确保我们的组件使用 Jest 中的toMatchSnapshot
函数呈现预定义的 UI 布局。
调用此方法后,Jest 会自动创建一个名为[testFileName].snap
的快照文件,并将其添加到__snapshots__
文件夹中。
该文件表示我们期望从组件渲染中获得的 UI 布局。
但是,考虑到我们正在尝试做纯TDD,我们应该先创建这个文件,然后调用toMatchSnapshot
函数使测试失败。
考虑到我们不知道 Jest 使用哪种格式来表示此布局,这听起来可能有点令人困惑。
您可能想先执行toMatchSnapshot
函数并在快照文件中查看结果,这是一个有效的选项。 但是,如果我们真的想使用纯TDD,我们需要了解快照文件的结构。
快照文件包含与测试名称匹配的布局。 这意味着如果我们的测试有这种形式:
desc("ComponentA" () => { it("should do something", () => { … } });
我们应该在导出部分指定这一点: Component A should do something 1
。
您可以在此处阅读有关快照测试的更多信息。
因此,我们首先创建MyComponent.test.js.snap
文件。
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;
然后,我们创建将检查快照是否与组件子元素匹配的单元测试。
// MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...
我们可以将components.getElements
视为 render 方法的结果。
我们将这些元素传递给expect
方法,以便针对快照文件运行验证。
执行测试后,我们得到以下错误:
Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []
Jest 告诉我们component.getElements
的结果与快照不匹配。 因此,我们通过在MyComponent
中添加 input 元素来通过此测试。

// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }
下一步是通过在其值更改时执行函数来向input
添加功能。 我们通过在onChange
中指定一个函数来做到这一点。
我们首先需要更改快照以使测试失败。
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;
首先修改快照的一个缺点是道具(或属性)的顺序很重要。
Jest 将在根据快照验证它之前按字母顺序对expect
函数中收到的道具进行排序。 因此,我们应该按该顺序指定它们。
执行测试后,我们得到以下错误:
Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]
为了使这个测试通过,我们可以简单地为onChange
提供一个空函数。
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }
然后,我们确保组件的状态在onChange
事件被调度后发生变化。
为此,我们创建了一个新的单元测试,它将通过传递一个事件来调用输入中的onChange
函数,以模拟 UI 中的真实事件。
然后,我们验证组件状态是否包含一个名为input
的键。
// MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });
我们现在得到以下错误。
Expected value to be defined, instead received undefined
这表明组件在名为input
的状态下没有属性。
我们通过将此条目设置为组件的状态来使测试通过。
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }
然后,我们需要确保在新状态条目中设置了一个值。 我们将从事件中得到这个值。
所以,让我们创建一个测试来确保状态包含这个值。
// MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""
我们最终通过从事件中获取值并将其设置为输入值来通过此测试。
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }
在确保所有测试都通过后,我们可以重构我们的代码。
我们可以将onChange
中传递的函数提取到一个名为updateState
的新函数中。
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }
我们现在有一个使用 TDD 创建的简单 React.js 组件。
概括
在这个例子中,我们尝试使用纯TDD,按照每一步编写尽可能少的代码来失败和通过测试。
有些步骤可能看起来没有必要,我们可能会想跳过它们。 但是,每当我们跳过任何步骤时,我们最终都会使用不太纯的 TDD 版本。
使用不太严格的 TDD 过程也是有效的,并且可能工作得很好。
我给你的建议是避免跳过任何步骤,如果你觉得困难也不要难过。 TDD 是一种不容易掌握的技术,但绝对值得一试。
如果您有兴趣了解有关 TDD 和相关行为驱动开发 (BDD) 的更多信息,请阅读 Toptaler Ryan Wilcox 的《你的老板不会欣赏 TDD》。