React 组件如何让 UI 测试变得简单

已发表: 2022-03-11

测试后端很容易。 你选择你喜欢的语言,将它与你最喜欢的框架配对,编写一些测试,然后点击“运行”。 你的控制台说“耶! 有用!” 您的持续集成服务会在每次推送时运行您的测试,生活很美好。

当然,测试驱动开发 (TDD) 起初很奇怪,但是可预测的环境、多个测试运行器、嵌入框架的测试工具以及持续集成支持,让生活变得轻松。 五年前,我认为测试可以解决我遇到的所有问题。

然后Backbone变大了。

我们都切换到前端 MVC。 我们的可测试后端变成了美化的数据库服务器。 我们最复杂的代码移到了浏览器中。 而且我们的应用程序在实践中不再可测试。

那是因为测试前端代码和 UI 组件有点困难。

如果我们只想检查我们的模型是否表现良好,那还不错。 或者,调用函数将更改正确的值。 对于 React 单元测试,我们需要做的就是:

  • 编写格式良好、独立的模块。
  • 使用 Jasmine 或 Mocha 测试(或其他)来运行函数。
  • 使用测试运行器,例如 Karma 或 Chutzpah。

而已。 我们的代码经过单元测试。

过去,运行前端测试是最困难的部分。 每个框架都有自己的想法,在大多数情况下,您最终会得到一个浏览器窗口,每次您想要运行测试时都需要手动刷新它。 当然,你总是会忘记。 至少,我知道我做到了。

2012 年,Vojta Jina 发布了 Karma runner(当时称为 Testacular)。 借助 Karma,前端测试成为工具链的完整公民。 我们的 React 测试在终端或持续集成服务器上运行,当我们更改文件时它们会重新运行,我们甚至可以同时在多个浏览器中测试我们的代码。

我们还能期望什么? 好吧,来实际测试我们的前端代码。

前端测试需要的不仅仅是单元测试

单元测试很棒:它是查看算法是否每次都做正确的事情,或检查我们的输入验证逻辑、数据转换或任何其他孤立操作的最佳方法。 单元测试非常适合基础。

但是前端代码不是关于操纵数据的。 它是关于用户事件和在正确的时间呈现正确的视图。 前端是关于用户的。

这是我们希望能够做到的:

  • 测试 React 用户事件
  • 测试对这些事件的响应
  • 确保在正确的时间呈现正确的内容
  • 在许多浏览器中运行测试
  • 对文件更改重新运行测试
  • 使用 Travis 等持续集成系统

在我一直这样做的十年中,直到我开始研究 React 之前,我还没有找到一种体面的方法来测试用户交互和视图渲染。

React 单元测试:UI 组件

React 是实现这些目标的最简单方法。 部分是因为它迫使我们使用可测试的模式来构建应用程序,部分是因为有很棒的 React 测试工具。

如果你以前从未使用过 React,你应该看看我的书React+d3.js 。 它面向可视化,但有人告诉我它是 React 的“一个很棒的轻量级介绍”

React 迫使我们将所有东西都构建为“组件”。 您可以将 React 组件视为小部件,或具有某些逻辑的 HTML 块。 它们遵循函数式编程的许多最佳原则,但它们是对象。

例如,给定相同的参数集,React 组件将始终呈现相同的输出。 不管它被渲染了多少次,不管是谁渲染它,不管我们把输出放在哪里。 总是一样。 因此,我们不必执行复杂的脚手架来测试 React 组件。 他们只关心他们的属性,不需要跟踪全局变量和配置对象。

我们在很大程度上通过避免状态来实现这一点。 您可以在函数式编程中将此称为引用透明性。 我不认为 React 中有专门的名称,但官方文档建议尽可能避免使用 state。

在测试用户交互时,React 为我们提供了绑定到函数回调的事件。 设置测试间谍并确保点击事件调用正确的函数很容易。 而且因为 React 组件会呈现自己,所以我们可以触发一个点击事件并检查 HTML 是否有变化。 这是有效的,因为 React 组件只关心它自己。 点击这里不会改变那里的事情。 我们将永远不必处理嵌套的事件处理程序,只需定义良好的函数调用。

哦,因为 React 很神奇,我们不必担心 DOM。 React 使用所谓的虚拟 DOM 将组件渲染为 JavaScript 变量。 对虚拟 DOM 的引用是我们测试 React 组件所需要的,真的。

它很甜。

React 的TestUtils

React 带有一套内置的TestUtils 。 甚至还有一个推荐的名为 Jest 的测试运行程序,但我不喜欢它。 我稍后会解释为什么。 首先, TestUtils

我们通过类似require('react/addons').addons.TestUtils获得它们。 这是我们测试用户交互和检查输出的入口点。

React TestUtils让我们通过将它的 DOM 放在一个变量中来渲染一个 React 组件,而不是将它插入到页面中。 例如,要渲染一个 React 组件,我们会这样做:

 var component = TestUtils.renderIntoDocument( <MyComponent /> );

然后我们可以使用TestUtils来检查是否所有的孩子都被渲染了。 像这样的东西:

 var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );

findRenderedDOMComponentWithTag会做它听起来的样子:遍历孩子,找到我们正在寻找的组件,然后返回它。 返回的值将表现得像一个 React 组件。

然后我们可以使用getDOMNode()来访问原始 DOM 元素并测试它的值。 要检查组件中的h1标签是否显示“A title” ,我们会这样写:

 expect(h1.getDOMNode().textContent) .toEqual("A title");

放在一起,完整的测试看起来像这样:

 it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });

很酷的部分是 TestUtils 还允许我们触发用户事件。 对于点击事件,我们会这样写:

 var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);

这会模拟单击并触发任何潜在的侦听器,这些侦听器应该是更改输出、状态或两者的组件方法。 如有必要,这些侦听器可以调用父组件上的函数。

所有情况都易于测试:更改后的状态在component.state中,我们可以使用普通的 DOM 函数访问输出,并使用间谍进行函数调用。

为什么不开玩笑?

React 的官方文档推荐使用 https://facebook.github.io/jest/ 作为测试运行器和 React 测试框架。 Jest 基于 Jasmine 构建并使用相同的语法。 除了从 Jasmine 获得的所有内容之外,Jest 还模拟除我们正在测试的组件之外的所有内容。 这在理论上很棒,但我觉得很烦人。 我们还没有实现的任何东西,或者来自代码库的不同部分的东西,都只是undefined 。 虽然这在许多情况下都很好,但它可能会导致错误的悄悄失败。

例如,我在测试点击事件时遇到了麻烦。 无论我尝试什么,它都不会调用它的侦听器。 然后我意识到这个函数被 Jest 嘲笑了,它从来没有告诉我这个。

但到目前为止,Jest 最糟糕的问题是它没有自动测试新更改的监视模式。 我们可以运行一次,得到测试结果,就是这样。 (我喜欢在工作时在后台运行我的测试。否则我会忘记运行它们。)现在这不再是一个问题。

哦,Jest 不支持在多个浏览器中运行 React 测试。 这个问题比以前少了,但我觉得这是一个重要的功能,因为只有在特定版本的 Chrome 中才会出现 heisenbug 的罕见情况……

编者按:自从本文最初编写以来,Jest 有了很大的改进。 您可以阅读我们最近的教程,使用 Enzyme 和 Jest 进行反应单元测试,并自行决定 Jest 测试是否能够胜任当今的任务。

React 测试:一个集成的例子

无论如何,我们已经看到了一个好的前端 React 测试在理论上应该如何工作。 让我们用一个简短的例子来付诸行动。

我们将使用由 React 和 d3.js 制作的散点图组件来可视化生成随机数的不同方式。 代码及其演示也在 Github 上。

我们将使用 Karma 作为测试运行器,Mocha 作为测试框架,Webpack 作为模块加载器。

设置

我们的源文件将放在<root>/src目录中,我们将把测试放在<root>/src/__tests__目录中。 这个想法是我们可以在src中放置几个​​目录,每个主要组件一个目录,每个目录都有自己的测试文件。 像这样捆绑源代码和测试文件可以更轻松地在不同项目中重用 React 组件。

有了目录结构,我们可以像这样安装依赖项:

 $ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect

如果有任何安装失败,请尝试重新运行安装的该部分。 NPM 有时会以重新运行时消失的方式失败。

完成后,我们的package.json文件应如下所示:

 // package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }

经过一些配置后,我们将能够使用npm testkarma start运行测试。

运行测试

配置

配置不多。 我们必须确保 Webpack 知道如何找到我们的代码,并且 Karma 知道如何运行测试。

我们在./tests.webpack.js文件中放置了两行 JavaScript,以帮助 Karma 和 Webpack 协同工作:

 // tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);

这告诉 Webpack 考虑将任何带有-test后缀的东西作为测试套件的一部分。

配置 Karma 需要更多的工作:

 // karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };

这些行中的大多数来自默认的 Karma 配置。 我们使用browsers表示测试应该在 Chrome 中运行,使用frameworks来指定我们正在使用的测试框架,并使用singleRun来让测试默认只运行一次。 您可以使用karma start --no-single-run让 karma 在后台运行。

这三个很明显。 Webpack 的东西更有趣。

因为 Webpack 处理我们代码的依赖树,所以我们不必在files数组中指定所有文件。 我们只需要tests.webpack.js ,然后它需要所有必要的文件。

我们使用webpack设置来告诉 Webpack 要做什么。 在正常环境中,这部webpack.config.js文件中。

我们还告诉 Webpack 为我们的 JavaScript 使用babel-loader 。 这为我们提供了来自 ECMAScript2015React 的 JSX 的所有花哨的新功能。

通过webpackServer配置,我们告诉 Webpack 不要打印任何调试信息。 它只会破坏我们的测试输出。

一个 React 组件和一个测试

有了一个正在运行的测试套件,剩下的就很简单了。 我们必须制作一个接受随机坐标数组的组件,并创建一个带有一堆点的<svg>元素。

遵循 React 测试最佳实践——即标准 TDD 实践——我们将首先编写测试,然后才是真正的 React 组件。 让我们从src/__tests__/中的普通测试文件开始:

 // ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });

首先,我们需要 React、它的 TestUtils、d3.js、 expect库和我们正在测试的代码。 然后我们用describe创建一个新的测试套件,并创建一些随机数据。

对于我们的第一个测试,让我们确保ScatterPlot呈现标题。 我们的测试进入describe块:

 // ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });

大多数测试将遵循相同的模式:

  1. 使成为。
  2. 查找特定节点。
  3. 检查内容。

正如我们之前看到的, renderIntoDocument渲染我们的组件, findRenderedDOMComponentWithTag找到我们正在测试的特定部分, getDOMNode为我们提供原始 DOM 访问。

起初我们的测试会失败。 为了让它通过,我们必须编写呈现标题标签的组件:

 var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;

而已。 ScatterPlot组件使用包含预期文本的<h1>标签呈现<div> ,我们的测试将通过。 是的,它不仅仅是 HTML,但请耐心等待。

画出猫头鹰的其余部分

如上所述,您可以在 GitHub 上查看我们示例的其余部分。 本文将跳过一步一步的描述,但大体过程与上面相同。 不过,我确实想向您展示一个更有趣的测试。 确保所有数据点都显示在图表上的测试:

 // ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });

和之前一样。 渲染,查找节点,检查结果。 这里有趣的部分是绘制那些 DOM 节点。 我们向ScatterPlot组件添加一些 d3.js 魔法,如下所示:

 // ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...

我们使用componentWillMountXY域设置空的 d3 比例,并使用componentWillReceiveProps确保它们在发生变化时得到更新。 然后update_d3确保为两个比例设置domainrange

我们将使用这两个尺度在数据集中的随机值和图片上的位置之间进行转换。 大多数随机生成器返回[0,1]范围内的数字,该范围太小而无法视为像素。

然后我们将这些点添加到组件的 render 方法中:

 // ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }

此代码遍历this.props.data数组并为每个数据点添加一个<circle>元素。 简单的。

使用 React 组件测试不再对 UI 测试感到绝望。
鸣叫

如果你想了解更多关于结合 React 和 d3.js 来制作数据可视化组件的信息,那是查看我的书React+d3.js的另一个重要原因。

自动化 React 组件测试:比听起来容易

这就是我们使用 React 编写可测试的前端组件所需要了解的全部内容。 要查看更多测试 React 组件的代码,请查看 Github 上的 React 测试示例代码库,如上所述。

我们了解到:

  1. React 迫使我们模块化和封装。
  2. 这使得 React UI 测试很容易自动化。
  3. 单元测试对于前端来说是不够的。
  4. 业力是一个伟大的测试跑步者。
  5. Jest 有潜力,但还没有完全成熟。 (或者现在可能是。)

如果您喜欢这篇文章,请在 Twitter 上关注我并在下方发表评论。 感谢阅读,祝 React 测试愉快!

相关:如何优化组件以提高 React 性能