Reactjs のテストを ava + jsdom で書いてみる


## 目的

ava + jsdom がどういうものかさらっと触れてみる。
ついでに以前書いた [ReactJS + facebook/flux を ES6 の記述でカウンターのサンプル](http://qiita.com/cortyuming/items/bf24a087b4831ffa20f8) をリファクタしてみた。

## ファイル構成

```
.
├── index.html
├── js
│   └── bundle.js
├── package.json
├── src
│   ├── app.jsx
│   └── app_dispatcher.js
└── test
    ├── hoge.html
    └── hoge_test.js
```

## package


```pacage.json
{
  "name": "ava_and_jsdom",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "browserify src/app.jsx -o js/bundle.js --debug",
    "watch": "watchify src/app.jsx -o js/bundle.js -v --debug",
    "test": "ava"
  },
  "browserify": {
    "transform": [
      [
        "babelify",
        {
          "presets": [
            "es2015",
            "react"
          ]
        }
      ]
    ]
  },
  "author": "Takashi ITO",
  "license": "MIT",
  "devDependencies": {
    "ava": "^0.10.0",
    "babel-preset-es2015": "^6.3.13",
    "babel-preset-react": "^6.3.13",
    "babelify": "^7.2.0",
    "browserify": "^13.0.0",
    "delay": "^1.3.1",
    "jsdom": "^7.2.2",
    "watchify": "^3.7.0"
  },
  "dependencies": {
    "flux": "^2.1.1",
    "react": "^0.14.6",
    "react-dom": "^0.14.6"
  }
}
```

### インストールとかビルドとか

```
$ npm install
$ npm run watch (or npm run build)
```

## コード

```src/app.jsx
'use strict';

import AppDispatcher from './app_dispatcher';
import React from 'react';
import ReactDOM from 'react-dom';
import {EventEmitter} from 'events';


// Store
class CounterStore extends EventEmitter {
  constructor() {
    super();

    AppDispatcher.register(this._onAction.bind(this));

    this.CHANGE_EVENT = 'change';
    this.counter = 0;
  }

  _onAction(action) {
    switch(action.actionType) {
      case 'COUNTER/UPDATE':
        this.counter += action.num;
        this.emitChange();
        break;
    }
  }

  emitChange() {
    this.emit(this.CHANGE_EVENT);
  }

  addChangeListener(callback) {
    this.on(this.CHANGE_EVENT, callback);
  }

  removeChangeListener(callback) {
    this.removeListener(this.CHANGE_EVENT, callback);
  }

  getState() {
    return {count: this.counter};
  }
}
const counterStore = new CounterStore();


// Action creators
const CounterActions = {
  update: function(num) {
    AppDispatcher.dispatch({
      actionType: 'COUNTER/UPDATE',
      num: num
    });
  }
};


// Component
class CounterBtnComponent extends React.Component {
  render() {
    return (
      <div>
        <div>
          <button onClick={this.onClick.bind(this)}>+1</button>
          <button onClick={this.onClick.bind(this)}>-1</button>
        </div>
      </div>
    );
  }

  onClick(event) {
    CounterActions.update(Number(event.target.textContent));
  }
}


class AppComponent extends React.Component {
  constructor() {
    super();
    this.state = counterStore.getState();
  }

  componentDidMount() {
    counterStore.addChangeListener(this._onChange.bind(this));
  }

  componentWillUnmount() {
    counterStore.removeChangeListener(this._onChange.bind(this));
  }

  render() {
    return (
      <div>
        <span>count: {this.state.count}</span>
        <CounterBtnComponent />
      </div>
    );
  }

  _onChange() {
    this.setState(counterStore.getState());
  }
}


ReactDOM.render(
  <AppComponent />,
  document.querySelector('.js-container')
);
```

```src/app_dispatcher.js
var Dispatcher = require('flux').Dispatcher;

module.exports = new Dispatcher();
```

```index.html
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>React+ava+jsdom</title>
    </head>
    <body>
        <div class="js-container"></div>

        <script src="js/bundle.js"></script>
    </body>
</html>
```

## 表示

```
$ python -m SimpleHTTPServer 8000
```

- http://localhost:8000


![スクリーンショット 2015-07-03 0.20.12.png](https://qiita-image-store.s3.amazonaws.com/0/41656/3fb3784f-2d3e-965e-eff8-33d1bc7e8d04.png "スクリーンショット 2015-07-03 0.20.12.png")


## テストまわり

```test/hoge.html
<div class="js-container"></div>
```

```test/hoge_test.js
'use strict';

import test from 'ava';
import jsdom from 'jsdom';
import fs from 'fs';


const hoge_html = fs.readFileSync('hoge.html', 'utf-8');
const dom = {
  then: (onFulfill, onReject) => {
    const config = {
      html: hoge_html,
      scripts: ['../js/bundle.js'],
      done: (err, window) => {
        if (err) {
          onReject(err);
        } else {
          onFulfill(window);
        }
      }
    };

    jsdom.env(config);
  }
};
const SELECTOR = {
  counterNum: 'body > div.js-container > div > span > span:nth-child(2)',
  plusBtn: 'body > div.js-container > div > div > div > button:nth-child(1)',
  minusBtn: 'body > div.js-container > div > div > div > button:nth-child(2)'
};


test('initial counter num', t => {
  return Promise.resolve(dom)
    .then((window) => {
      let counterNum = window.document.querySelector(SELECTOR.counterNum).textContent;
      t.is(counterNum, '0');
    });
});

test('1 increases when click +1 button', t => {
  return Promise.resolve(dom)
    .then((window) => {
      let plusBtn = window.document.querySelector(SELECTOR.plusBtn);
      plusBtn.click();
      return window;
    })
    .then((window) => {
      let counterNum = window.document.querySelector(SELECTOR.counterNum).textContent;
      t.is(counterNum, '1');
    });
});

test('1 decreases when click -1 button', t => {
  return Promise.resolve(dom)
    .then((window) => {
      let plusBtn = window.document.querySelector(SELECTOR.minusBtn);
      plusBtn.click();
      return window;
    })
    .then((window) => {
      let counterNum = window.document.querySelector(SELECTOR.counterNum).textContent;
      t.is(counterNum, '-1');
    });
});
```

### [ava のアサーション](https://github.com/sindresorhus/ava#assertions)

- .pass([message])
- .fail([message])
- .ok(value, [message])
- .notOk(value, [message])
- .true(value, [message])
- .false(value, [message])
- .is(value, expected, [message])
- .not(value, expected, [message])
- .same(value, expected, [message])
- .notSame(value, expected, [message])
- .throws(function|promise, error, [message])
- .doesNotThrow(function|promise, [message])
- .ifError(error, [message])


### テスト実行

```
$ npm test

> sample@1.0.0 test /Users/username/sample/
> ava

  3 passed
```

## 感想

- 想定してたより書きやすそうかも。
- ava は mocha より高速らしいけど計測して無いのでよくわからない。（比較記事ないのかな？）




## 参考

- https://github.com/sindresorhus/ava
- https://github.com/tmpvar/jsdom