测试是开发周期中的一个重要组成部分。没有测试的代码被称为:遗留代码。对于我而言,第一次学习 React 和 JavaScript 的时候,感到很有压力。如果你也是刚开始学习 JS/React,并加入他们的社区,那么也可能会有相同的感觉。想到的会是:
- 我应该用哪一个构建工具?
- 哪一个测试框架比较好?
- 我应该学习哪种流模式?
- 我需要用到流吗?
为了解决这些烦恼,我决定写这篇文章。经过几个小时的博客文章阅读,查阅 JS 开发者的源码,还有参加 Florida 的 JSConf,终于让我找到了自己的测试“槽”。开始让我觉得没有经过测试的 React 程序代码是如此的不标准和凌乱。我想活在一个没有这种感觉的世界,但后来想想,这是不对的。
本教程所有的代码都可以在我的 github 仓库中找到。
让我们开始吧!
设置 Webpack
本教程不是一个教如何使用 webpack,所以我不会详细说,但重要的是要了解基本的东西。
Webpack 就像 Rails 中的 Assets Pipeline 一样。在基础层面上而言,在运行 react 应用时,
会在处理你的代码和服务的前后,只生成一个 bundle.js
在客户端。
因为它是一个非常强大的工具,所以我们会常常用到。在开始,Webpack 的功能可能会吓到你,
但我建议你坚持使用下去,一旦你了解了其中的原理,就会觉得得心应手。而你只需给它一个机会去表现。
通常我们不会喜欢那些我们不会的,或是害怕的。然而,一旦你克服初始不适并开始理解它,总会变得很有趣。事实上,这正是我对测试的感受。当开始时讨厌它,在熟悉后喜欢它 :-)
如果感兴趣,这里有一些资源来更多地了解关于 webpack:
- Webpack Cookbook(使用的是 Babel 5,但对于学习 Webpack 的基本原理而言还是很有用的)
- Webpack 初学者可以看这篇文章
- Pete Hunts 所写的 Webpack How-to
注意:如果要持续随本教程实验,建议使用 Node 版本为
v5.1.0
。当然版本>4
的也是可以的。
首先,安装所有关于 webpack 和 babel 的依赖。Babel 是一个转译器,允许你在开发时使用 ES6(es2015)和 ES7 的特性,然后将这些代码转译成浏览器可以识别的 ES5 代码。
1 | mkdir tdd_react |
npm i
是 npm install 的别名。
接下来,让我们设置项目的路径和创建一个 webpack.config.js
文件:
1 | mkdir src # where all our source code will live |
初始化的 webpack config 会很小。阅读这些注释,理解下发生了什么:
1 | // our webpack.config.js file located in project root |
为了让 babel 更好地工作,我们需要定义哪个 presets
是我们需要用到的。让我们继续,并且安装 React 和 ES6 预处理所需的东西:
1 | npm i babel-preset-react babel-preset-es2015 --save-dev |
现在我们有一些选项。在 webpack config 文件中,会告诉你哪一块是做 bebel 预处理的:
1 | loaders: [ |
另外的方法是将他们存在 .babelrc
文件中,这也用在我的项目中。将 babel 预处理存储在 .babelrc
中,对于以后的开发者而言,更容易去找到哪个 babel 预处理是可用的。此外,当我们将 Karma 设置到 webpack 之后,因为 .babelrc
文件的存在,我们就不再需要其他的预处理配置了。
1 | # inside our project root |
将下面这段粘贴到预处理文件中:
1 | # .babelrc |
为了确认它能否工作,让我们在 main.js
中加入一些 react 代码,并看看所有的包是否正常。接着安装 React 和 React DOM:
1 | npm i react react-dom -S |
使用
-S
是--save
的别名。
创建第一个 React 组件:
1 | # src/main.js |
聪明的读者就会察觉我们并没有在根部创建一个 index.html
文件。让我们继续,当 bundle.js
编译后,将其放到 /dist
文件夹中:
1 | # /dist/index.html |
非常棒,让我们继续。最后,我们可以运行 webpack,看看一切是否正常。如果你没有全局安装 webpack(npm i webpack -g
),你也可以用 node modules 方式进行启动:
1 | ./node_modules/.bin/webpack |
Webpack 将默认情况下寻找一个配置名称为 webpack.config.js
。如果你高兴,也可以通过不同 webpack config 作为参数传入。
在 package.json 中创建一个别名,来完成构建工作:
1 | # package.json |
接下来让 webpack-dev-server
提升开发体验:
1 | npm i webpack-dev-server --save-dev |
将 webpack dev server 的入口加入到 webpack.config.js
中:
1 | ... rest of config |
让 script 运行在开发服务器上:
1 | # package.json |
在 script 中使用了 --content-base
标记,告诉 webpack 我们想服务于 /dist
文件夹。我们还定义了 3000 端口,使得更像是 Rails 开发的体验。
最后,在 webpack 配置文件中添加一个 resolve 标记,使进口文件看起来更直观。下面就是配置文件最终的样子:
1 | var webpack = require('webpack'); |
为确保一切工作正常,让我们运行开发服务器,并且确认我们在屏幕上看到 “Hello World”。
1 | npm run dev |
你应该看到的是这样的:
设置 Mocha,Chai,Sinon 和 Enzyme
Mocha:将用于运行我们的测试。
Chai:是我们期待的库。应用非常广泛,允许使用 RSpec 一样的语法。
Sinon:将服务于 mocks/stubs/spies.
Enzyme:将用于测试我们的 React components。AirBnB 写的一个很漂亮的测试库。
安装这些包:
1 | npm i mocha chai sinon --save-dev |
如果我们希望能够使用 ES6 编写测试,那么我们需要在运行前对代码进行转译。那么我们需要安装 babel-register:
1 | npm i babel-register --save-dev |
加一些 npm scripts 到 package.json
中,让测试更简单:
1 | # ./package.json |
我们的测试脚本要运行 mocha,并使用 babel-register
进行转译,然后递归地查看 /test
目录。
最终,我们需要设置 Karma,因此 npm script 会变得无效,但如果不设置,它将会正常工作。npm run test:watch
将会监视程序,并在文件发生修改时重新运行。多么高效!
确认它能工作,创建一个 hello world 测试 /tests/helloWorld.spec.js
:
1 | # /test/helloWorld.spec.js |
哇…看起来很像 RSpec!
如果每一个测试都要引入 expect
,这将变得很麻烦,因此让我们新建一个 test_helper
文件来保存这些东西:
1 | # /test/test_helper.js |
然后把它包括到 npm 脚本的运行套件中,并通过 --require ./test/test_helper.js
来声明:
1 | # package.json script section |
我也添加了 sinon,因此它也可以全局可用。现在无论什么时候,我们在写一个新的测试时,都不需要手动引入 expect
和 sinon
。
Enzyme
现在我们所需的“普通”测试工具都已经设置好了(mocha,chai,sinon),接着让我们安装 Enzyme,并且开始测试 React component!
安装这个包:
1 | npm i enzyme react-addons-test-utils --save-dev |
Enzyme 的重要文档可以在这里找到。如果有时间,我推荐阅读 Shallow Rendering 部分。
你会问,什么是 Shallow Rendering?
对我们来说是一种组件调用 render 方法,得到我们可以断言的 React 元素,而无需实际安装组件到 DOM 上。更多的 React 元素请看这。
Enzyme 会将 shallow rendered 组件包裹进一个特殊的 wrapper
中,进而让我们可以测试。如果你用过 Rails,这看起来像是 Capybara 中的 page
对象。
让我们为一些合适的 <Root />
组件进行 TDD 的驱动开发。
这个 Root 组件会是一个 container
,意味着在应用中它可以控制 state 的处理。学习 React 中“智能”和“笨拙”组件之间的差异,对于应用程序体系结构是很重要的。这篇文章很好地解释了它们。
1 | # /tests/containers/Root.spec.js |
如果我们用 npm test
运行测试,这会失败。因为我们没有在适当的位置创建一个根组件。因此我们可以这样做:
如果在任何时候你想看到这段代码的源代码,可以在 github 仓库 中找到
1 | # /src/containers/Root.js |
重新运行测试就可以了。
在我们的测试中有很多重复的东西,因此我们还需要回去做一些重构。由于我们没有给 Root
传入任何的 props,那么我们可以 shallow render 它一次,然后就在一个 wrapper 中结束了我们所有的断言。很多时候给定一个特定的 props 后,我发现自己包装的部分测试会在 “sub” describe 块中,然后给一堆断言也有这些 props。如果你用过 RSpec,就类似于使用 “context” 块。
1 | describe('(Container) Root', () => { |
尽可能地在你的测试中使用 shallow
,但偶尔也可能不用。例如,如果你要测试 React 生命周期的方法时,就需要真正地将组件安装出来。
接下来让我们测试一个组件的安装和调用函数,当它安装时,我们可以得到一些暴露在 sinon
上的信息和正在使用的 spies。
我们可以假装 Root
组件有一个子组件叫 CommentList
,在安装后将调用任意的回调。当通过给定 props 组件安装时,函数被调用,因此我们就可以测试这个场景。在组件渲染时给评论列表一些 style,然后我们就可以知道 shallow render 是如何处理这些样式的了。CommentList
会在一个组件文件夹的 /src/components/CommentList.js
中。因为它不处理数据,因此完全取决于 props,换句话说它是一个纯(笨拙)组件:
1 | import React from 'react'; |
还有很多,阅读这些注释可以帮助你更好地理解。看看这些实践,让测试可以通过,然后再回头看看这些测试,验证下你所理解的东西。
1 | # /src/components/CommentList.js |
运行 npm test
,现在这些套件应该可以通过测试了。
接下来让我们添加一些 shallow rendered 测试,当给定一个 isActive
的 props 时,来确保我们的组件使用了适当的 CSS class。
1 | ... previous tests |
让它们通过测试:
1 | class CommentList extends Component { |
此时你应该对如何测试 react 组件已经有了一个很好的理解了。记得去阅读 Enzyme 文档来获得更多的灵感。
设置 Karma
设置 Karma 可能会有些困难。坦白讲,这对我而言也是一件痛苦的工作。通常,当我开发 React 应用时,我会选择使用已经构建好的 starter kit,方便省事。我非常推荐开发时用的 starter kit。
使用 Karma 的价值在于快速测试重载,可以多浏览器测试和最重要的是 webpack 预处理。一旦我们将 Karma 设置好了,在我们运行测试程序时,不仅是只有 babel-loader
,而是整个 webpack config。这为我们提供了很多便利,使得我们的测试环境与开发环境相同。
让我们开始吧…
1 | npm i karma karma-chai karma-mocha karma-webpack --save-dev |
很多包,我知道。相信我完成这个是非常值得的。
对于我们的示例而言,我们将使用 PhantomJS。没有别的什么原因,这我在 starter kit 中已经用到了。可以按照自己的喜好使用 Chrome,Firefox 或是 Safari,甚至在 PhantomJS 之上。(这是用 Karma 的一件很酷的事)
在配置 karma 之前先安装 yargs
,它能让你使用命令行参数来定制 Karma 的配置。
1 | npm i yargs -S |
现在我们可以通过创建一个 Karma config 文件去监视我们的文件,当文件发生修改时重新运行并很快地保存。
Karma Config:1
touch karma.config.js
1 | // ./karma.config.js |
阅读所有的注释一次或两次有助于理解这个 config 是做什么的。使用 Webpack 的一大好处是全部都是普通的 JavaScript 代码,并且我们可以“重构”配置文件。事实上,这正是绝大多数 starter kit 所做的!
随着 Karma 设置完成,为运行测试,最后一件事就是要去更新我们的 package.json:
1 | # package.json |
我建议重命名旧的测试 scripts 的前缀,用 ‘old_’ 表示。
最终的 package.json
是这样的:
1 | { |
在测试套件中外加 webpack 预处理,我们现在可以删除那些在测试内烦人的相对路径声明:
1 | // test/containers/Root.spec.js |
现在使用这个 starter kit 开发,你需要输入以下这些命令去运行程序:
1 | npm run dev # note the addition of run |
如果还有什么不清楚的地方,可以在 github 上查看该源码。
结论
我们已经建立了一个坚实的测试环境,可以根据你的项目具体需求去改变和发展。在下一次的文章中,我将花更多的时间在特殊场景的测试,还有如何测试 Redux,我更喜欢 flux 的实现。
虽然我只使用 React 开发了数月,但我已经爱上它了。我希望本教程可以帮助你更深入地理解一些 React 测试的最佳实践。有任何问题或评论随时联系我。测试是我们的好朋友!