测试是开发周期中的一个重要组成部分。没有测试的代码被称为:遗留代码。对于我而言,第一次学习 React 和 JavaScript 的时候,感到颇有压力。若是你也是刚开始学习 JS/React,并加入他们的社区,那么也可能会有相同的感受。想到的会是:javascript
为了解决这些烦恼,我决定写这篇文章。通过几个小时的博客文章阅读,查阅 JS 开发者的源码,还有参加 Florida 的 JSConf,终于让我找到了本身的测试“槽”。开始让我以为没有通过测试的 React 程序代码是如此的不标准和凌乱。我想活在一个没有这种感受的世界,但后来想一想,这是不对的。html
本教程全部的代码均可以在个人 github 仓库中找到。java
让咱们开始吧!node
本教程不是一个教如何使用 webpack,因此我不会详细说,但重要的是要了解基本的东西。
Webpack 就像 Rails 中的 Assets Pipeline 同样。在基础层面上而言,在运行 react 应用时,
会在处理你的代码和服务的先后,只生成一个 bundle.js
在客户端。react
由于它是一个很是强大的工具,因此咱们会经常用到。在开始,Webpack 的功能可能会吓到你,
但我建议你坚持使用下去,一旦你了解了其中的原理,就会以为驾轻就熟。而你只需给它一个机会去表现。webpack
一般咱们不会喜欢那些咱们不会的,或是惧怕的。然而,一旦你克服初始不适并开始理解它,总会变得颇有趣。事实上,这正是我对测试的感觉。当开始时讨厌它,在熟悉后喜欢它 :-)git
若是感兴趣,这里有一些资源来更多地了解关于 webpack:github
注意:若是要持续随本教程实验,建议使用 Node 版本为
v5.1.0
。固然版本>4
的也是能够的。web
首先,安装全部关于 webpack 和 babel 的依赖。Babel 是一个转译器,容许你在开发时使用 ES6(es2015)和 ES7 的特性,而后将这些代码转译成浏览器能够识别的 ES5 代码。npm
mkdir tdd_react cd tdd_react npm init # follow along with normal npm init to set up project npm i babel-loader babel-core webpack --save-dev
npm i
是 npm install 的别名。
接下来,让咱们设置项目的路径和建立一个 webpack.config.js
文件:
mkdir src # where all our source code will live touch src/main.js # this will be the entry point for our webpack bundling mkdir test # place to store all our tests mkdir dist # this is where the bundled javascript from webpack will go touch webpack.config.js # our webpack configuration file
初始化的 webpack config 会很小。阅读这些注释,理解下发生了什么:
// our webpack.config.js file located in project root var webpack = require('webpack'); var path = require('path'); // a useful node path helper library var config = { entry: ['./src/main.js'], // the entry point for our app output: { path: path.resolve(__dirname, 'dist'), // store the bundled output in dist/bundle.js filename: 'bundle.js' // specifying file name for our compiled assets }, module: { loaders: [ // telling webpack which loaders we want to use. For now just run the // code through the babel-loader. 'babel' is an alias for babel-loader { test: /\.js$/, loaders: ['babel'], exclude: /node_modules/ } ] } } module.exports = config;
为了让 babel 更好地工做,咱们须要定义哪一个 presets
是咱们须要用到的。让咱们继续,而且安装 React 和 ES6 预处理所需的东西:
npm i babel-preset-react babel-preset-es2015 --save-dev
如今咱们有一些选项。在 webpack config 文件中,会告诉你哪一块是作 bebel 预处理的:
loaders: [ { test: /\.js$/, loaders: ['babel'], exclude: /node_modules/, query: { presets: ['react', 'es2015'] } } ]
另外的方法是将他们存在 .babelrc
文件中,这也用在个人项目中。将 babel 预处理存储在 .babelrc
中,对于之后的开发者而言,更容易去找到哪一个 babel 预处理是可用的。此外,当咱们将 Karma 设置到 webpack 以后,由于 .babelrc
文件的存在,咱们就再也不须要其余的预处理配置了。
# inside our project root touch .babelrc
将下面这段粘贴到预处理文件中:
# .babelrc { "presets": ["react", "es2015"] }
为了确认它可否工做,让咱们在 main.js
中加入一些 react 代码,并看看全部的包是否正常。接着安装 React 和 React DOM:
npm i react react-dom -S
使用
-S
是--save
的别名。
建立第一个 React 组件:
# src/main.js import React, { Component } from 'react'; import { render } from 'react-dom'; class Root extends Component { render() { return <h1> Hello World </h1>; } } render(<Root />, document.getElementById('root'));
聪明的读者就会察觉咱们并无在根部建立一个 index.html
文件。让咱们继续,当 bundle.js
编译后,将其放到 /dist
文件夹中:
# /dist/index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> </head> <body> <div id="root"></div> <script src="bundle.js"></script> </body> </html>
很是棒,让咱们继续。最后,咱们能够运行 webpack,看看一切是否正常。若是你没有全局安装 webpack(npm i webpack -g
),你也能够用 node modules 方式进行启动:
./node_modules/.bin/webpack
Webpack 将默认状况下寻找一个配置名称为 webpack.config.js
。若是你高兴,也能够经过不一样 webpack config 做为参数传入。
在 package.json 中建立一个别名,来完成构建工做:
# package.json ... other stuff "scripts": { "build": "webpack" }
接下来让 webpack-dev-server
提高开发体验:
npm i webpack-dev-server --save-dev
将 webpack dev server 的入口加入到 webpack.config.js
中:
... rest of config entry: [ 'webpack/hot/dev-server', 'webpack-dev-server/client?http://localhost:3000', './src/main.js' ], ... rest of config
让 script 运行在开发服务器上:
# package.json ... other stuff scripts: { "dev": "webpack-dev-server --port 3000 --devtool eval --progress --colors --hot --content-base dist", "build": "webpack" }
在 script 中使用了 --content-base
标记,告诉 webpack 咱们想服务于 /dist
文件夹。咱们还定义了 3000 端口,使得更像是 Rails 开发的体验。
最后,在 webpack 配置文件中添加一个 resolve 标记,使进口文件看起来更直观。下面就是配置文件最终的样子:
var webpack = require('webpack'); var path = require('path'); var config = { entry: [ 'webpack/hot/dev-server', 'webpack-dev-server/client?http://localhost:3000', './src/main.js' ], resolve: { root: [ // allows us to import modules as if /src was the root. // so I can do: import Comment from 'components/Comment' // instead of: import Comment from '../components/Comment' or whatever relative path would be path.resolve(__dirname, './src') ], // allows you to require without the .js at end of filenames // import Component from 'component' vs. import Component from 'component.js' extensions: ['', '.js', '.json', '.jsx'] }, output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { loaders: [ { test: /\.js?$/, // dont run node_modules or bower_components through babel loader exclude: /(node_modules|bower_components)/, // babel is alias for babel-loader // npm i babel-core babel-loader --save-dev loader: 'babel' } ], } } module.exports = config;
为确保一切工做正常,让咱们运行开发服务器,而且确认咱们在屏幕上看到 “Hello World”。
npm run dev open http://localhost:3000
你应该看到的是这样的:
Mocha:将用于运行咱们的测试。
Chai:是咱们期待的库。应用很是普遍,容许使用 RSpec 同样的语法。
Sinon:将服务于 mocks/stubs/spies.
Enzyme:将用于测试咱们的 React components。AirBnB 写的一个很漂亮的测试库。
安装这些包:
npm i mocha chai sinon --save-dev
若是咱们但愿可以使用 ES6 编写测试,那么咱们须要在运行前对代码进行转译。那么咱们须要安装 babel-register:
npm i babel-register --save-dev
加一些 npm scripts 到 package.json
中,让测试更简单:
# ./package.json ... rest of package.json "scripts": { "test": "mocha --compilers js:babel-register --recursive", "test:watch": "npm test -- --watch", "build": "webpack", "dev": "webpack-dev-server --port 3000 --devtool eval --progress --colors --hot --content-base dist", },
咱们的测试脚本要运行 mocha,并使用 babel-register
进行转译,而后递归地查看 /test
目录。
最终,咱们须要设置 Karma,所以 npm script 会变得无效,但若是不设置,它将会正常工做。npm run test:watch
将会监视程序,并在文件发生修改时从新运行。多么高效!
确认它能工做,建立一个 hello world 测试 /tests/helloWorld.spec.js
:
# /test/helloWorld.spec.js import { expect } from 'chai'; describe('hello world', () => { it('works!', () => { expect(true).to.be.true; }); });
哇...看起来很像 RSpec!
若是每个测试都要引入 expect
,这将变得很麻烦,所以让咱们新建一个 test_helper
文件来保存这些东西:
# /test/test_helper.js import { expect } from 'chai'; import sinon from 'sinon'; global.expect = expect; global.sinon = sinon;
而后把它包括到 npm 脚本的运行套件中,并经过 --require ./test/test_helper.js
来声明:
# package.json script section "test": "mocha --compilers js:babel-register --require ./test/test_helper.js --recursive",
我也添加了 sinon,所以它也能够全局可用。如今不管何时,咱们在写一个新的测试时,都不须要手动引入 expect
和 sinon
。
如今咱们所需的“普通”测试工具都已经设置好了(mocha,chai,sinon),接着让咱们安装 Enzyme,而且开始测试 React component!
安装这个包:
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 中“智能”和“笨拙”组件之间的差别,对于应用程序体系结构是很重要的。这篇文章很好地解释了它们。
# /tests/containers/Root.spec.js import React from 'react'; // required to get test to work. we can get around this later with more configuration import { shallow } from 'enzyme'; // method from enzyme which allows us to do shallow render import Root from '../../src/containers/Root'; // import our soon to be component describe('(Container) Root', () => { it('renders as a <div>', () => { const wrapper = shallow(<Root />); expect(wrapper.type()).to.eql('div'); }); it('has style with height 100%', () => { const wrapper = shallow(<Root />); const expectedStyles = { height: '100%', background: '#333' } expect(wrapper.prop('style')).to.eql(expectedStyles); }); it('contains a header explaining the app', () => { const wrapper = shallow(<Root />); expect(wrapper.find('.welcome-header')).to.have.length(1); }); });
若是咱们用 npm test
运行测试,这会失败。由于咱们没有在适当的位置建立一个根组件。所以咱们能够这样作:
若是在任什么时候候你想看到这段代码的源代码,能够在 github 仓库 中找到
# /src/containers/Root.js import React, { Component } from 'react'; const styles = { height: '100%', background: '#333' } class Root extends Component { render() { return ( <div style={styles}> <h1 className='welcome-header'>Welcome to testing React!</h1> </div> ) } } export default Root;
从新运行测试就能够了。
在咱们的测试中有不少重复的东西,所以咱们还须要回去作一些重构。因为咱们没有给 Root
传入任何的 props,那么咱们能够 shallow render 它一次,而后就在一个 wrapper 中结束了咱们全部的断言。不少时候给定一个特定的 props 后,我发现本身包装的部分测试会在 “sub” describe 块中,而后给一堆断言也有这些 props。若是你用过 RSpec,就相似于使用 “context” 块。
describe('(Container) Root', () => { const wrapper = shallow(<Root />); it('renders as a <div>', () => { expect(wrapper.type()).to.eql('div'); }); it('has style with height 100%', () => { const expectedStyles = { height: '100%', background: '#333' } expect(wrapper.prop('style')).to.eql(expectedStyles); }); it('contains a header explaining the app', () => { expect(wrapper.find('.welcome-header')).to.have.length(1); }); });
尽量地在你的测试中使用 shallow
,但偶尔也可能不用。例如,若是你要测试 React 生命周期的方法时,就须要真正地将组件安装出来。
接下来让咱们测试一个组件的安装和调用函数,当它安装时,咱们能够获得一些暴露在 sinon
上的信息和正在使用的 spies。
咱们能够伪装 Root
组件有一个子组件叫 CommentList
,在安装后将调用任意的回调。当经过给定 props 组件安装时,函数被调用,所以咱们就能够测试这个场景。在组件渲染时给评论列表一些 style,而后咱们就能够知道 shallow render 是如何处理这些样式的了。CommentList
会在一个组件文件夹的 /src/components/CommentList.js
中。由于它不处理数据,所以彻底取决于 props,换句话说它是一个纯(笨拙)组件:
import React from 'react'; // Once we set up Karma to run our tests through webpack // we will no longer need to have these long relative paths import CommentList from '../../src/components/CommentList'; import { describeWithDOM, mount, shallow, spyLifecycle } from 'enzyme'; describe('(Component) CommentList', () => { // using special describeWithDOM helper that enzyme // provides so if other devs on my team don't have JSDom set up // properly or are using old version of node it won't bork their test suite // // All of our tests that depend on mounting should go inside one of these // special describe blocks describeWithDOM('Lifecycle methods', () => { it('calls componentDidMount', () => { spyLifecyle(CommentList); const props = { onMount: () => {}, // an anonymous function in ES6 arrow syntax isActive: false } // using destructuring to pass props down // easily and then mounting the component mount(<CommentList {...props} />); // CommentList's componentDidMount should have been // called once. spyLifecyle attaches sinon spys so we can // make this assertion expect( CommentList.prototype.componentDidMount.calledOnce ).to.be.true; }); it('calls onMount prop once it mounts', () => { // create a spy for the onMount function const props = { onMount: sinon.spy() }; // mount our component mount(<CommentList {...props} />); // expect that onMount was called expect(props.onMount.calledOnce).to.be.true; }); }); });
还有不少,阅读这些注释能够帮助你更好地理解。看看这些实践,让测试能够经过,而后再回头看看这些测试,验证下你所理解的东西。
# /src/components/CommentList.js import React, { Component, PropTypes } from 'react'; const propTypes = { onMount: PropTypes.func.isRequired, isActive: PropTypes.bool }; class CommentList extends Component { componentDidMount() { this.props.onMount(); } render() { return ( <ul> <li> Comment One </li> </ul> ) } } CommentList.propTypes = propTypes; export default CommentList;
运行 npm test
,如今这些套件应该能够经过测试了。
接下来让咱们添加一些 shallow rendered 测试,当给定一个 isActive
的 props 时,来确保咱们的组件使用了适当的 CSS class。
... previous tests it('should render as a <ul>', () => { const props = { onMount: () => {} }; const wrapper = shallow(<CommentList {...props} />); expect(wrapper.type()).to.eql('ul'); }); describe('when active...', () => { const wrapper = shallow( // just passing isActive is an alias for true <CommentList onMount={() => {}} isActive /> ) it('should render with className active-list', () => { expect(wrapper.prop('className')).to.eql('active-list'); }); }); describe('when inactive...', () => { const wrapper = shallow( <CommentList onMount={() => {}} isActive={false} /> ) it('should render with className inactive-list', () => { expect(wrapper.prop('className')).to.eql('inactive-list'); }); }); });
让它们经过测试:
class CommentList extends Component { componentDidMount() { this.props.onMount(); } render() { const { isActive } = this.props; const className = isActive ? 'active-list' : 'inactive-list'; return ( <ul className={className}> <li> Comment One </li> </ul> ) } }
此时你应该对如何测试 react 组件已经有了一个很好的理解了。记得去阅读 Enzyme 文档来得到更多的灵感。
设置 Karma 可能会有些困难。坦白讲,这对我而言也是一件痛苦的工做。一般,当我开发 React 应用时,我会选择使用已经构建好的 starter kit,方便省事。我很是推荐开发时用的 starter kit。
使用 Karma 的价值在于快速测试重载,能够多浏览器测试和最重要的是 webpack 预处理。一旦咱们将 Karma 设置好了,在咱们运行测试程序时,不只是只有 babel-loader
,而是整个 webpack config。这为咱们提供了不少便利,使得咱们的测试环境与开发环境相同。
让咱们开始吧...
npm i karma karma-chai karma-mocha karma-webpack --save-dev npm i karma-sourcemap-loader karma-phantomjs-launcher --save-dev npm i karma-spec-reporter --save-dev npm i phantomjs --save-dev # The polyfills arn't required but will help with browser support issues # and are easy enough to include in our karma config that I figured why not npm i babel-polyfill phantomjs-polyfill --save-dev
不少包,我知道。相信我完成这个是很是值得的。
对于咱们的示例而言,咱们将使用 PhantomJS。没有别的什么缘由,这我在 starter kit 中已经用到了。能够按照本身的喜爱使用 Chrome,Firefox 或是 Safari,甚至在 PhantomJS 之上。(这是用 Karma 的一件很酷的事)
在配置 karma 以前先安装 yargs
,它能让你使用命令行参数来定制 Karma 的配置。
npm i yargs -S
如今咱们能够经过建立一个 Karma config 文件去监视咱们的文件,当文件发生修改时从新运行并很快地保存。
Karma Config:
touch karma.config.js
// ./karma.config.js var argv = require('yargs').argv; var path = require('path'); module.exports = function(config) { config.set({ // only use PhantomJS for our 'test' browser browsers: ['PhantomJS'], // just run once by default unless --watch flag is passed singleRun: !argv.watch, // which karma frameworks do we want integrated frameworks: ['mocha', 'chai'], // displays tests in a nice readable format reporters: ['spec'], // include some polyfills for babel and phantomjs files: [ 'node_modules/babel-polyfill/dist/polyfill.js', './node_modules/phantomjs-polyfill/bind-polyfill.js', './test/**/*.js' // specify files to watch for tests ], preprocessors: { // these files we want to be precompiled with webpack // also run tests throug sourcemap for easier debugging ['./test/**/*.js']: ['webpack', 'sourcemap'] }, // A lot of people will reuse the same webpack config that they use // in development for karma but remove any production plugins like UglifyJS etc. // I chose to just re-write the config so readers can see what it needs to have webpack: { devtool: 'inline-source-map', resolve: { // allow us to import components in tests like: // import Example from 'components/Example'; root: path.resolve(__dirname, './src'), // allow us to avoid including extension name extensions: ['', '.js', '.jsx'], // required for enzyme to work properly alias: { 'sinon': 'sinon/pkg/sinon' } }, module: { // don't run babel-loader through the sinon module noParse: [ /node_modules\/sinon\// ], // run babel loader for our tests loaders: [ { test: /\.js?$/, exclude: /node_modules/, loader: 'babel' }, ], }, // required for enzyme to work properly externals: { 'jsdom': 'window', 'cheerio': 'window', 'react/lib/ExecutionEnvironment': true, 'react/lib/ReactContext': 'window' }, }, webpackMiddleware: { noInfo: true }, // tell karma all the plugins we're going to be using to prevent warnings plugins: [ 'karma-mocha', 'karma-chai', 'karma-webpack', 'karma-phantomjs-launcher', 'karma-spec-reporter', 'karma-sourcemap-loader' ] }); };
阅读全部的注释一次或两次有助于理解这个 config 是作什么的。使用 Webpack 的一大好处是所有都是普通的 JavaScript 代码,而且咱们能够“重构”配置文件。事实上,这正是绝大多数 starter kit 所作的!
随着 Karma 设置完成,为运行测试,最后一件事就是要去更新咱们的 package.json:
# package.json "scripts" { "test": "node_modules/.bin/karma start karma.config.js", "test:dev": "npm run test -- --watch", "old_test": "mocha --compilers js:babel-register --require ./test/test_helper.js --recursive", "old_test:watch": "npm test -- --watch" }
我建议重命名旧的测试 scripts 的前缀,用 'old_' 表示。
最终的 package.json
是这样的:
{ "name": "react-testing-starter-kit", "version": "0.1.0", "description": "React starter kit with nice testing environment set up.", "main": "src/main.js", "directories": { "test": "tests", "src": "src", "dist": "dist" }, "dependencies": { "react": "^0.14.6", "react-dom": "^0.14.6", "yargs": "^3.31.0" }, "devDependencies": { "babel-core": "^6.4.0", "babel-loader": "^6.2.1", "babel-polyfill": "^6.3.14", "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", "babel-register": "^6.3.13", "chai": "^3.4.1", "enzyme": "^1.2.0", "json-loader": "^0.5.4", "karma": "^0.13.19", "karma-chai": "^0.1.0", "karma-mocha": "^0.2.1", "karma-phantomjs-launcher": "^0.2.3", "karma-sourcemap-loader": "^0.3.6", "karma-spec-reporter": "0.0.23", "karma-webpack": "^1.7.0", "mocha": "^2.3.4", "phantomjs": "^1.9.19", "phantomjs-polyfill": "0.0.1", "react-addons-test-utils": "^0.14.6", "sinon": "^1.17.2", "webpack": "^1.12.11", "webpack-dev-server": "^1.14.1" }, "scripts": { "test": "node_modules/.bin/karma start karma.config.js", "test:dev": "npm run test -- --watch", "build": "webpack", "dev": "webpack-dev-server --port 3000 --devtool eval --progress --colors --hot --content-base dist", "old_test": "mocha --compilers js:babel-register --require ./test/test_helper.js --recursive", "old_test:watch": "npm test -- --watch" }, "repository": { "type": "git", "url": "tbd" }, "author": "Spencer Dixon", "license": "ISC" }
在测试套件中外加 webpack 预处理,咱们如今能够删除那些在测试内烦人的相对路径声明:
// test/containers/Root.spec.js import React from 'react'; import { shallow } from 'enzyme'; import Root from 'containers/Root'; // new import statement // import Root from '../../src/containers/Root'; // old import statement // test/components/CommentList.spec.js import React from 'react'; import CommentList from 'components/CommentList'; // new import statement // import CommentList from '../../src/components/CommentList'; // old import statement import { describeWithDOM, mount, shallow, spyLifecycle } from 'enzyme';
如今使用这个 starter kit 开发,你须要输入如下这些命令去运行程序:
npm run dev # note the addition of run npm run test:dev # note the addition of run
若是还有什么不清楚的地方,能够在 github 上查看该源码。
咱们已经创建了一个坚实的测试环境,能够根据你的项目具体需求去改变和发展。在下一次的文章中,我将花更多的时间在特殊场景的测试,还有如何测试 Redux,我更喜欢 flux 的实现。
虽然我只使用 React 开发了数月,但我已经爱上它了。我但愿本教程能够帮助你更深刻地理解一些 React 测试的最佳实践。有任何问题或评论随时联系我。测试是咱们的好朋友!
做者:Jovey连接:http://www.jianshu.com/p/6c74c96148c9來源:简书著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。