測試是開發週期中的一個重要組成部分。沒有測試的代碼被稱爲:遺留代碼。對於我而言,第一次學習 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來源:簡書著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。