React 測(cè)試驅(qū)動(dòng)教程

歡迎移步我的博客閱讀:《React 測(cè)試驅(qū)動(dòng)教程》

測(cè)試是開發(fā)周期中的一個(gè)重要組成部分。沒有測(cè)試的代碼被稱為:遺留代碼哼丈。對(duì)于我而言,第一次學(xué)習(xí) React 和 JavaScript 的時(shí)候,感到很有壓力累奈。如果你也是剛開始學(xué)習(xí) JS/React朝扼,并加入他們的社區(qū)丐膝,那么也可能會(huì)有相同的感覺绍赛。想到的會(huì)是:

  • 我應(yīng)該用哪一個(gè)構(gòu)建工具?
  • 哪一個(gè)測(cè)試框架比較好鸣个?
  • 我應(yīng)該學(xué)習(xí)哪種流模式羞反?
  • 我需要用到流嗎布朦?

為了解決這些煩惱,我決定寫這篇文章昼窗。經(jīng)過幾個(gè)小時(shí)的博客文章閱讀是趴,查閱 JS 開發(fā)者的源碼,還有參加 Florida 的 JSConf澄惊,終于讓我找到了自己的測(cè)試“槽”唆途。開始讓我覺得沒有經(jīng)過測(cè)試的 React 程序代碼是如此的不標(biāo)準(zhǔn)和凌亂。我想活在一個(gè)沒有這種感覺的世界掸驱,但后來想想肛搬,這是不對(duì)的。

本教程所有的代碼都可以在我的 github 倉(cāng)庫(kù)中找到毕贼。

讓我們開始吧温赔!

設(shè)置 Webpack

本教程不是一個(gè)教如何使用 webpack,所以我不會(huì)詳細(xì)說鬼癣,但重要的是要了解基本的東西陶贼。
Webpack 就像 Rails 中的 Assets Pipeline 一樣。在基礎(chǔ)層面上而言待秃,在運(yùn)行 react 應(yīng)用時(shí)拜秧,
會(huì)在處理你的代碼和服務(wù)的前后,只生成一個(gè) bundle.js 在客戶端锥余。

因?yàn)樗且粋€(gè)非常強(qiáng)大的工具腹纳,所以我們會(huì)常常用到痢掠。在開始驱犹,Webpack 的功能可能會(huì)嚇到你,
但我建議你堅(jiān)持使用下去足画,一旦你了解了其中的原理雄驹,就會(huì)覺得得心應(yīng)手。而你只需給它一個(gè)機(jī)會(huì)去表現(xiàn)淹辞。

通常我們不會(huì)喜歡那些我們不會(huì)的医舆,或是害怕的。然而象缀,一旦你克服初始不適并開始理解它蔬将,總會(huì)變得很有趣。事實(shí)上央星,這正是我對(duì)測(cè)試的感受霞怀。當(dāng)開始時(shí)討厭它,在熟悉后喜歡它 :-)

如果感興趣莉给,這里有一些資源來更多地了解關(guān)于 webpack:

  1. Webpack Cookbook(使用的是 Babel 5毙石,但對(duì)于學(xué)習(xí) Webpack 的基本原理而言還是很有用的)
  2. Webpack 初學(xué)者可以看這篇文章
  3. Pete Hunts 所寫的 Webpack How-to

注意:如果要持續(xù)隨本教程實(shí)驗(yàn)廉沮,建議使用 Node 版本為 v5.1.0。當(dāng)然版本 >4 的也是可以的徐矩。

首先滞时,安裝所有關(guān)于 webpack 和 babel 的依賴。Babel 是一個(gè)轉(zhuǎn)譯器滤灯,允許你在開發(fā)時(shí)使用 ES6(es2015)和 ES7 的特性坪稽,然后將這些代碼轉(zhuǎn)譯成瀏覽器可以識(shí)別的 ES5 代碼。

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 的別名鳞骤。

接下來刽漂,讓我們?cè)O(shè)置項(xiàng)目的路徑和創(chuàng)建一個(gè) 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 會(huì)很小。閱讀這些注釋弟孟,理解下發(fā)生了什么:

// 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 更好地工作贝咙,我們需要定義哪個(gè) presets 是我們需要用到的。讓我們繼續(xù)拂募,并且安裝 React 和 ES6 預(yù)處理所需的東西:

npm i babel-preset-react babel-preset-es2015 --save-dev

現(xiàn)在我們有一些選項(xiàng)庭猩。在 webpack config 文件中,會(huì)告訴你哪一塊是做 bebel 預(yù)處理的:

loaders: [
  {
    test: /\.js$/,
    loaders: ['babel'],
    exclude: /node_modules/,
    query: {
      presets: ['react', 'es2015']
    }
  }
]

另外的方法是將他們存在 .babelrc 文件中陈症,這也用在我的項(xiàng)目中蔼水。將 babel 預(yù)處理存儲(chǔ)在 .babelrc 中,對(duì)于以后的開發(fā)者而言录肯,更容易去找到哪個(gè) babel 預(yù)處理是可用的趴腋。此外,當(dāng)我們將 Karma 設(shè)置到 webpack 之后论咏,因?yàn)?.babelrc 文件的存在优炬,我們就不再需要其他的預(yù)處理配置了。

# inside our project root
touch .babelrc

將下面這段粘貼到預(yù)處理文件中:

# .babelrc
{
  "presets": ["react", "es2015"]
}

為了確認(rèn)它能否工作厅贪,讓我們?cè)?main.js 中加入一些 react 代碼蠢护,并看看所有的包是否正常。接著安裝 React 和 React DOM:

npm i react react-dom -S

使用 -S--save 的別名养涮。

創(chuàng)建第一個(gè) 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'));

聰明的讀者就會(huì)察覺我們并沒有在根部創(chuàng)建一個(gè) index.html 文件葵硕。讓我們繼續(xù),當(dāng) 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>

非常棒懈凹,讓我們繼續(xù)。最后悄谐,我們可以運(yùn)行 webpack介评,看看一切是否正常。如果你沒有全局安裝 webpack(npm i webpack -g)尊沸,你也可以用 node modules 方式進(jìn)行啟動(dòng):

./node_modules/.bin/webpack

Webpack 將默認(rèn)情況下尋找一個(gè)配置名稱為 webpack.config.js威沫。如果你高興贤惯,也可以通過不同 webpack config 作為參數(shù)傳入。

在 package.json 中創(chuàng)建一個(gè)別名棒掠,來完成構(gòu)建工作:

# package.json
... other stuff
"scripts": {
  "build": "webpack"
}

接下來讓 webpack-dev-server 提升開發(fā)體驗(yàn):

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 運(yùn)行在開發(fā)服務(wù)器上:

# package.json
... other stuff
scripts: {
  "dev": "webpack-dev-server --port 3000 --devtool eval --progress --colors --hot --content-base dist",
  "build": "webpack"
}

在 script 中使用了 --content-base 標(biāo)記孵构,告訴 webpack 我們想服務(wù)于 /dist 文件夾。我們還定義了 3000 端口烟很,使得更像是 Rails 開發(fā)的體驗(yàn)颈墅。

最后,在 webpack 配置文件中添加一個(gè) resolve 標(biāo)記雾袱,使進(jìn)口文件看起來更直觀恤筛。下面就是配置文件最終的樣子:

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;

為確保一切工作正常,讓我們運(yùn)行開發(fā)服務(wù)器芹橡,并且確認(rèn)我們?cè)谄聊簧峡吹?“Hello World”毒坛。

npm run dev
open http://localhost:3000

你應(yīng)該看到的是這樣的:

Hello World Image

設(shè)置 Mocha,Chai林说,Sinon 和 Enzyme

Mocha:將用于運(yùn)行我們的測(cè)試煎殷。
Chai:是我們期待的庫(kù)。應(yīng)用非常廣泛腿箩,允許使用 RSpec 一樣的語法豪直。
Sinon:將服務(wù)于 mocks/stubs/spies.
Enzyme:將用于測(cè)試我們的 React components。AirBnB 寫的一個(gè)很漂亮的測(cè)試庫(kù)珠移。

安裝這些包:

npm i mocha chai sinon --save-dev

如果我們希望能夠使用 ES6 編寫測(cè)試弓乙,那么我們需要在運(yùn)行前對(duì)代碼進(jìn)行轉(zhuǎn)譯。那么我們需要安裝 babel-register:

npm i babel-register --save-dev

加一些 npm scripts 到 package.json 中钧惧,讓測(cè)試更簡(jiǎn)單:

# ./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",
  },

我們的測(cè)試腳本要運(yùn)行 mocha暇韧,并使用 babel-register 進(jìn)行轉(zhuǎn)譯,然后遞歸地查看 /test 目錄垢乙。

最終锨咙,我們需要設(shè)置 Karma,因此 npm script 會(huì)變得無效追逮,但如果不設(shè)置,它將會(huì)正常工作粹舵。npm run test:watch 將會(huì)監(jiān)視程序钮孵,并在文件發(fā)生修改時(shí)重新運(yùn)行。多么高效眼滤!

確認(rèn)它能工作巴席,創(chuàng)建一個(gè) hello world 測(cè)試 /tests/helloWorld.spec.js

# /test/helloWorld.spec.js
import { expect } from 'chai';

describe('hello world', () => {
  it('works!', () => {
    expect(true).to.be.true;
  });
});

哇...看起來很像 RSpec!

如果每一個(gè)測(cè)試都要引入 expect诅需,這將變得很麻煩漾唉,因此讓我們新建一個(gè) test_helper 文件來保存這些東西:

# /test/test_helper.js
import { expect } from 'chai';
import sinon from 'sinon';

global.expect = expect;
global.sinon = sinon;

然后把它包括到 npm 腳本的運(yùn)行套件中荧库,并通過 --require ./test/test_helper.js 來聲明:

# package.json script section
  "test": "mocha --compilers js:babel-register --require ./test/test_helper.js --recursive",

我也添加了 sinon,因此它也可以全局可用≌孕蹋現(xiàn)在無論什么時(shí)候分衫,我們?cè)趯懸粋€(gè)新的測(cè)試時(shí),都不需要手動(dòng)引入 expectsinon般此。

Enzyme

現(xiàn)在我們所需的“普通”測(cè)試工具都已經(jīng)設(shè)置好了(mocha蚪战,chai,sinon)铐懊,接著讓我們安裝 Enzyme邀桑,并且開始測(cè)試 React component!

安裝這個(gè)包:

npm i enzyme react-addons-test-utils --save-dev

Enzyme 的重要文檔可以在這里找到科乎。如果有時(shí)間壁畸,我推薦閱讀 Shallow Rendering 部分。

你會(huì)問茅茂,什么是 Shallow Rendering瓤摧?

對(duì)我們來說是一種組件調(diào)用 render 方法,得到我們可以斷言的 React 元素玉吁,而無需實(shí)際安裝組件到 DOM 上照弥。更多的 React 元素請(qǐng)看這

Enzyme 會(huì)將 shallow rendered 組件包裹進(jìn)一個(gè)特殊的 wrapper 中进副,進(jìn)而讓我們可以測(cè)試这揣。如果你用過 Rails,這看起來像是 Capybara 中的 page 對(duì)象影斑。

讓我們?yōu)橐恍┖线m的 <Root /> 組件進(jìn)行 TDD 的驅(qū)動(dòng)開發(fā)给赞。

這個(gè) Root 組件會(huì)是一個(gè) container,意味著在應(yīng)用中它可以控制 state 的處理矫户。學(xué)習(xí) React 中“智能”和“笨拙”組件之間的差異片迅,對(duì)于應(yīng)用程序體系結(jié)構(gòu)是很重要的。這篇文章很好地解釋了它們皆辽。

# /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 運(yùn)行測(cè)試柑蛇,這會(huì)失敗。因?yàn)槲覀儧]有在適當(dāng)?shù)奈恢脛?chuàng)建一個(gè)根組件驱闷。因此我們可以這樣做:

如果在任何時(shí)候你想看到這段代碼的源代碼耻台,可以在 github 倉(cāng)庫(kù) 中找到

# /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;

重新運(yùn)行測(cè)試就可以了。

在我們的測(cè)試中有很多重復(fù)的東西空另,因此我們還需要回去做一些重構(gòu)盆耽。由于我們沒有給 Root 傳入任何的 props,那么我們可以 shallow render 它一次,然后就在一個(gè) wrapper 中結(jié)束了我們所有的斷言摄杂。很多時(shí)候給定一個(gè)特定的 props 后坝咐,我發(fā)現(xiàn)自己包裝的部分測(cè)試會(huì)在 “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);
  });
});

盡可能地在你的測(cè)試中使用 shallow氮昧,但偶爾也可能不用框杜。例如,如果你要測(cè)試 React 生命周期的方法時(shí)袖肥,就需要真正地將組件安裝出來咪辱。

接下來讓我們測(cè)試一個(gè)組件的安裝和調(diào)用函數(shù),當(dāng)它安裝時(shí)椎组,我們可以得到一些暴露在 sinon 上的信息和正在使用的 spies油狂。

我們可以假裝 Root 組件有一個(gè)子組件叫 CommentList,在安裝后將調(diào)用任意的回調(diào)寸癌。當(dāng)通過給定 props 組件安裝時(shí)专筷,函數(shù)被調(diào)用,因此我們就可以測(cè)試這個(gè)場(chǎng)景蒸苇。在組件渲染時(shí)給評(píng)論列表一些 style磷蛹,然后我們就可以知道 shallow render 是如何處理這些樣式的了。CommentList 會(huì)在一個(gè)組件文件夾的 /src/components/CommentList.js 中溪烤。因?yàn)樗惶幚頂?shù)據(jù)味咳,因此完全取決于 props,換句話說它是一個(gè)笨拙)組件:

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;
    });
  });
});

還有很多檬嘀,閱讀這些注釋可以幫助你更好地理解槽驶。看看這些實(shí)踐鸳兽,讓測(cè)試可以通過掂铐,然后再回頭看看這些測(cè)試,驗(yàn)證下你所理解的東西揍异。

# /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;

運(yùn)行 npm test 全陨,現(xiàn)在這些套件應(yīng)該可以通過測(cè)試了。

接下來讓我們添加一些 shallow rendered 測(cè)試蒿秦,當(dāng)給定一個(gè) isActive 的 props 時(shí)烤镐,來確保我們的組件使用了適當(dāng)?shù)?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');
    });
  });
});

讓它們通過測(cè)試:

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>
    )
  }
}

此時(shí)你應(yīng)該對(duì)如何測(cè)試 react 組件已經(jīng)有了一個(gè)很好的理解了棍鳖。記得去閱讀 Enzyme 文檔來獲得更多的靈感。

設(shè)置 Karma

設(shè)置 Karma 可能會(huì)有些困難。坦白講渡处,這對(duì)我而言也是一件痛苦的工作镜悉。通常,當(dāng)我開發(fā) React 應(yīng)用時(shí)医瘫,我會(huì)選擇使用已經(jīng)構(gòu)建好的 starter kit侣肄,方便省事。我非常推薦開發(fā)時(shí)用的 starter kit醇份。

使用 Karma 的價(jià)值在于快速測(cè)試重載稼锅,可以多瀏覽器測(cè)試和最重要的是 webpack 預(yù)處理。一旦我們將 Karma 設(shè)置好了僚纷,在我們運(yùn)行測(cè)試程序時(shí)矩距,不僅是只有 babel-loader,而是整個(gè) webpack config怖竭。這為我們提供了很多便利锥债,使得我們的測(cè)試環(huán)境與開發(fā)環(huán)境相同。

讓我們開始吧...

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

很多包痊臭,我知道哮肚。相信我完成這個(gè)是非常值得的。

對(duì)于我們的示例而言广匙,我們將使用 PhantomJS允趟。沒有別的什么原因,這我在 starter kit 中已經(jīng)用到了鸦致〕奔簦可以按照自己的喜好使用 Chrome,F(xiàn)irefox 或是 Safari蹋凝,甚至在 PhantomJS 之上鲁纠。(這是用 Karma 的一件很酷的事)

在配置 karma 之前先安裝 yargs,它能讓你使用命令行參數(shù)來定制 Karma 的配置鳍寂。

npm i yargs -S

現(xiàn)在我們可以通過創(chuàng)建一個(gè) Karma config 文件去監(jiān)視我們的文件改含,當(dāng)文件發(fā)生修改時(shí)重新運(yùn)行并很快地保存。

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'
    ]
  });
};

閱讀所有的注釋一次或兩次有助于理解這個(gè) config 是做什么的迄汛。使用 Webpack 的一大好處是全部都是普通的 JavaScript 代碼捍壤,并且我們可以“重構(gòu)”配置文件。事實(shí)上鞍爱,這正是絕大多數(shù) starter kit 所做的鹃觉!

隨著 Karma 設(shè)置完成,為運(yùn)行測(cè)試睹逃,最后一件事就是要去更新我們的 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"
  }

我建議重命名舊的測(cè)試 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"
}

在測(cè)試套件中外加 webpack 預(yù)處理,我們現(xiàn)在可以刪除那些在測(cè)試內(nèi)煩人的相對(duì)路徑聲明:

// 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';

現(xiàn)在使用這個(gè) starter kit 開發(fā)疗隶,你需要輸入以下這些命令去運(yùn)行程序:

npm run dev         # note the addition of run
npm run test:dev    # note the addition of run

如果還有什么不清楚的地方佑笋,可以在 github 上查看該源碼

結(jié)論

我們已經(jīng)建立了一個(gè)堅(jiān)實(shí)的測(cè)試環(huán)境斑鼻,可以根據(jù)你的項(xiàng)目具體需求去改變和發(fā)展蒋纬。在下一次的文章中,我將花更多的時(shí)間在特殊場(chǎng)景的測(cè)試坚弱,還有如何測(cè)試 Redux蜀备,我更喜歡 flux 的實(shí)現(xiàn)。

雖然我只使用 React 開發(fā)了數(shù)月荒叶,但我已經(jīng)愛上它了碾阁。我希望本教程可以幫助你更深入地理解一些 React 測(cè)試的最佳實(shí)踐。有任何問題或評(píng)論隨時(shí)聯(lián)系我停撞。測(cè)試是我們的好朋友瓷蛙!

原文鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市戈毒,隨后出現(xiàn)的幾起案子艰猬,更是在濱河造成了極大的恐慌,老刑警劉巖埋市,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冠桃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡道宅,警方通過查閱死者的電腦和手機(jī)食听,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來污茵,“玉大人樱报,你說我怎么就攤上這事∨⒌保” “怎么了迹蛤?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)襟士。 經(jīng)常有香客問我盗飒,道長(zhǎng),這世上最難降的妖魔是什么陋桂? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任逆趣,我火速辦了婚禮,結(jié)果婚禮上嗜历,老公的妹妹穿的比我還像新娘宣渗。我一直安慰自己抖所,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布落包。 她就那樣靜靜地躺著部蛇,像睡著了一般摊唇。 火紅的嫁衣襯著肌膚如雪咐蝇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天巷查,我揣著相機(jī)與錄音有序,去河邊找鬼。 笑死岛请,一個(gè)胖子當(dāng)著我的面吹牛旭寿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播崇败,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼盅称,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了后室?” 一聲冷哼從身側(cè)響起缩膝,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎岸霹,沒想到半個(gè)月后疾层,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贡避,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年痛黎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刮吧。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡湖饱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出杀捻,到底是詐尸還是另有隱情井厌,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布水醋,位于F島的核電站旗笔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏拄踪。R本人自食惡果不足惜蝇恶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望惶桐。 院中可真熱鬧撮弧,春花似錦潘懊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贸辈,卻和暖如春释树,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背擎淤。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工奢啥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嘴拢。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓桩盲,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親席吴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赌结,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容