11
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-01-16

目的

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

ファイル構成

.
├── 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

スクリーンショット 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 のアサーション

  • .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 より高速らしいけど計測して無いのでよくわからない。(比較記事ないのかな?)

参考

11
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?