本文概述
测试后端很容易。你可以选择自己的语言, 将其与你喜欢的框架配对, 编写一些测试, 然后点击”运行”。你的控制台上显示”是的!有用!”你的持续集成服务会在每一次推送中运行你的测试, 生活是美好的。
当然, 测试驱动开发(TDD)最初很奇怪, 但是可预测的环境, 多个测试运行程序, 嵌入框架的测试工具以及持续的集成支持使工作变得轻松。五年前, 我认为测试是解决我所遇到的每个问题的解决方案。
然后骨干变大了。
我们都切换到了前端MVC。我们可测试的后端成为了荣耀的数据库服务器。我们最复杂的代码移到了浏览器中。而且我们的应用程序在实践中不再可测试。
那是因为测试前端代码和UI组件有点困难。
如果我们只需要检查模型是否运行良好, 那还不错。或者, 调用函数将更改正确的值。我们对React单元测试所要做的就是:
- 编写格式正确的隔离模块。
- 使用Jasmine或Mocha测试(或其他测试)运行功能。
- 使用测试运行程序, 例如Karma或Chutzpah。
而已。我们的代码已经过单元测试。
过去, 运行前端测试是困难的部分。每个框架都有其自己的想法, 并且在大多数情况下, 你最终获得一个浏览器窗口, 每次你要运行测试时都将手动刷新该窗口。当然, 你将永远忘记。至少, 我知道我做到了。
在2012年, 伏伊塔·吉娜(Vojta Jina)发布了Karma跑步者(当时称为Testacular)。借助Karma, 前端测试已成为工具链的完整组成部分。我们的React测试运行在终端或持续集成服务器上, 当我们更改文件时它们会重新运行, 甚至可以同时在多个浏览器中测试代码。
我们还能希望什么?好吧, 实际测试一下我们的前端代码。
前端测试需要的不仅仅是单元测试
单元测试很棒:这是查看算法每次执行正确操作, 检查我们的输入验证逻辑, 数据转换或任何其他隔离操作的最佳方法。单元测试非常适合基础知识。
但是前端代码与操作数据无关。这与用户事件和在正确的时间呈现正确的视图有关。前端与用户有关。
这是我们想要做的:
- 测试React用户事件
- 测试对这些事件的响应
- 确保在正确的时间呈现正确的事物
- 在许多浏览器中运行测试
- 重新运行文件更改测试
- 与Travis等持续集成系统一起使用
在我从事此工作的十年中, 直到我开始在React上戳时, 才发现一种不错的方法来测试用户交互和查看渲染。
React单元测试:UI组件
React是实现这些目标的最简单方法。部分原因是因为它迫使我们使用可测试的模式来构建应用程序, 部分是因为有出色的React测试工具。
如果你以前从未使用过React, 请阅读我的书React + d3.js。它面向可视化, 但是我被告知它是React的”很棒的轻量级介绍”。
React迫使我们将所有内容构建为”组件”。你可以将React组件视为小部件, 或带有某些逻辑的HTML块。它们是对象, 遵循函数式编程的许多最佳原则。
例如, 给定相同的参数集, React组件将始终呈现相同的输出。无论渲染多少次, 无论是谁渲染, 无论我们将输出放置在哪里。总是一样。因此, 我们无需执行复杂的脚手架即可测试React组件。他们只关心它们的属性, 而不需要跟踪全局变量和配置对象。
我们在很大程度上避免了状态。在函数式编程中, 你将这种参照透明性称为。我认为React中对此没有特殊的名称, 但官方文档建议尽可能避免使用状态。
在测试用户交互时, React让我们讨论了绑定到函数回调的事件。设置测试间谍很容易, 并确保单击事件调用正确的功能。而且由于React组件会自行渲染, 因此我们只需触发click事件并检查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标签是否显示”标题”, 我们可以这样编写:
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还模拟了除我们正在测试的组件之外的所有内容。从理论上讲这很棒, 但是我觉得很烦。我们尚未实现的任何东西, 或者来自代码库其他部分的东西, 都尚未定义。尽管这在许多情况下都可以, 但是它可以导致静默地使错误失效。
例如, 我在测试点击事件时遇到了麻烦。无论我尝试了什么, 它都不会调用它的监听器。然后我意识到该功能被Jest嘲笑了, 它从来没有告诉过我。
但是到目前为止, Jest的最严重罪行是, 它没有监视模式来自动测试新更改。我们可以运行一次, 获取测试结果, 仅此而已。 (我喜欢在工作时在后台运行测试。否则, 我会忘记运行它们。)如今, 这不再是问题。
哦, Jest不支持在多个浏览器中运行React测试。这个问题比以前少了, 但是我觉得这是一个重要功能, 在极少数情况下, heisenbug仅在特定版本的Chrome中发生…
编者注:自从撰写本文以来, Jest有了长足的进步。你可以阅读我们最新的教程《使用酶和Jest进行React单元测试》, 并自己决定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 test或karma 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.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配置。我们使用浏览器来表示测试应该在Chrome中运行, 使用框架指定我们使用的测试框架, 并使用singleRun使测试默认情况下仅运行一次。你可以使用karma start –no-single-run使karma在后台运行。
这三个很明显。 Webpack的东西更有趣。
由于Webpack可以处理代码的依赖关系树, 因此我们不必在files数组中指定所有文件。我们只需要tests.webpack.js, 然后需要所有必要的文件。
我们使用webpack设置来告诉Webpack做什么。在正常环境中, 这部分将放入webpack.config.js文件中。
我们还告诉Webpack为我们的JavaScript使用babel-loader。这为我们提供了ECMAScript2015和React的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");
});
大多数测试将遵循相同的模式:
- 渲染。
- 查找特定的节点。
- 检查内容。
如我们先前所见, 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 d.y; }), d3.max(props.data, function (d) { return d.y; })])
.range([props.point_r, Number(props.height-props.point_r)]);
this.xScale
.domain([d3.min(props.data, function (d) { return d.x; }), d3.max(props.data, function (d) { return d.x; })])
.range([props.point_r, Number(props.width-props.point_r)]);
}, ...
我们使用componentWillMount为X和Y域设置空的d3缩放比例, 并使用componentWillReceiveProps来确保在发生更改时对它们进行更新。然后, update_d3确保为两个比例设置域和范围。
我们将使用这两个比例在数据集中的随机值和图片上的位置之间进行转换。大多数随机生成器返回的数字在[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测试示例代码库。
我们了解到:
- React迫使我们进行模块化和封装。
- 这使得React UI测试易于自动化。
- 单元测试还不够前端。
- 业力是一位出色的测试跑步者。
- 笑话有潜力, 但还没有。 (或者也许现在是。)
如果你喜欢本文, 请在Twitter上关注我, 并在下面发表评论。感谢你的阅读, 以及愉快的React测试!
相关:如何优化组件以提高React性能
评论前必须登录!
注册