个性化阅读
专注于IT技术分析

测试驱动的React.js开发:使用Enzyme和Jest进行React.js单元测试

本文概述

根据迈克尔·费瑟斯(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函数作为第一个参数的任何东西)进行比较。

使用酶来安装React.js组件

酶提供了一种挂载和遍历React.js组件树的机制。这将有助于我们访问其自身的属性和状态及其子项, 以便运行我们的断言。

酶为组件安装提供两种基本功能:浅安装和安装。浅层函数仅将根组件加载到内存中, 而mount则加载整个DOM树。

我们将结合使用Enzyme和Jest来安装React.js组件并对其进行声明。

TDD创建反应组件的步骤

建立环境

你可以看一下此仓库, 它具有运行此示例的基本配置。

我们使用以下版本:

{
  "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", () => {
    …
  }
});

我们应该在”导出”部分中对此进行指定:组件A应该执行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 []

开玩笑告诉我们, 来自component.getElements的结果与快照不匹配。因此, 我们通过在MyComponent中添加输入元素来使此测试通过。

// MyComponent.js
import React from 'react';
export default class MyComponent extends React.Component {
  render() {
    return <div><input type="text" /></div>;
  }
}

下一步是通过在函数值更改时执行一个函数来向输入中添加功能。我们通过在onChange属性中指定一个函数来实现。

我们首先需要更改快照以使测试失败。

//__snapshots__/MyComponent.test.js.snap
exports[`MyComponent should render initial layout 1`] = `
Array [
<div>
    <input
         onChange={[Function]}
         type="text"      
     />
</div>, ]
`;

首先修改快照的缺点是道具(或属性)的顺序很重要。

Jest将按期望的字母顺序对期望函数中收到的道具进行排序, 然后根据快照对其进行验证。因此, 我们应该按此顺序指定它们。

执行测试后, 我们得到以下错误:

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事件后, 组件的状态会发生变化。

为此, 我们创建了一个新的单元测试, 该单元测试将通过传递事件以在UI中模拟真实事件, 从而在输入中调用onChange函数。

然后, 我们验证组件状态是否包含名为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

这表示该组件在称为输入状态下没有属性。

我们通过将此条目设置为组件状态来进行测试通过。

// 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)的更多信息, 请阅读srcminier Ryan Wilcox同事介绍的”你的老板不会欣赏TDD”。

赞(0)
未经允许不得转载:srcmini » 测试驱动的React.js开发:使用Enzyme和Jest进行React.js单元测试

评论 抢沙发

评论前必须登录!