読者です 読者をやめる 読者になる 読者になる

量産型エンジニアの憂鬱

きっと僕は何物にもなれない。

Gitで試行錯誤したコミットをまとめたい

git

GitとCIサービスを連携していて、CIサーバの設定がうまくいかなくて何度もコミットしちゃうことありますよね。
amend をつけててもリモートリポジトリに push する段階でマージを余儀なくされたり。
--force で push すればよいのですが、あとから一連のコミットをまとめたい場合の操作。

以下のコマンドをうってログを表示

git log --oneline
2dae1e1 travis.yml修正
9bc8c31 travis.yml修正
ebb438c init. commit.

今回は travis.yml で試行錯誤したコミットをまとめたいと思います。

rebase を使います。まとめたい一連のコミットのひとつ前を指定。

git rebase -i ebb438c

すると編集画面が立ち上がります。

  1 pick 9bc8c31 travis.yml修正
  2 pick 2dae1e1 travis.yml修正
  3 
  4 # Rebase ebb438c..2dae1e1 onto ebb438c (1 command(s))
  5 #
  6 # Commands:
  7 # p, pick = use commit
  8 # r, reword = use commit, but edit the commit message
  9 # e, edit = use commit, but stop for amending
 10 # s, squash = use commit, but meld into previous commit
 11 # f, fixup = like "squash", but discard this commit's log message
 12 # x, exec = run command (the rest of the line) using shell
 13 # d, drop = remove commit
...

説明にある通り、s にすると一つ前のコミットとまとめてくれます。

  1 pick 9bc8c31 travis.yml修正
  2 s 2dae1e1 travis.yml修正

これで編集を保存して終了すると、新しいコミットメッセージを入力する画面になるので入力してできあがり。

あとは

git push --force

してやればまとまります。

でも force push は複数人で開発してるリポジトリにはやめといた方がいいですね。

Spring BootプロジェクトをIntelliJでJUnitするときプロファイルを指定したい

Java IntelliJ IDEA Spring Boot テスト

Spring Bootを使って開発してますが、Selenideによる画面テストを実施する際、ローカルではFirefox、CIサーバ上ではphantomjsドライバを使ってテストしています。
また、ローカルとCIサーバで利用するドライバの切り替えは application.properties と pom.xml のプロファイルで行ってます。

application.properties

# PhantomJSのexeファイルのパス
phantomjs.binary.path=@phantomjs.binary.path@
# Selenideで使用するブラウザ
selenide.browser=@selenide.browser@


pom.xml

<properties>
    <!-- デフォルトのプロパティ -->
    <selenide.browser>firefox</selenide.browser>
</properties>
...
<profiles>
    <profile>
        <!-- CIサーバ -->
        <id>ci</id>
        <properties>
            <phantomjs.binary.path>/usr/local/phantomjs/bin/phantomjs</phantomjs.binary.path>
            <selenide.browser>phantomjs</selenide.browser>
        </properties>
    </profile>
<profiles>

application.propertiesで値を @ で囲むことによって、 pom.xml に記述されたプロパティに置き換えてくれます。
また、 <profiles> を利用することによってそのプロパティを上書きすることができます。

上記の場合、

mvn test

を実行するとfirefoxドライバが指定され、

mvn test -Pci

とすると、phantomjsドライバが指定されます。

これでテストしてたのですが、ローカルではテスト通ったのにCIサーバ上ではテストこけることがしばしば。試しにローカルで phantomjs でテストするとやっぱりこける。

Maven から test を実行すればプロファイルの指定も出来ますが、失敗したテストだけ再実行などができない。テスト毎にログを見ることができない。
など、やっぱり IntelliJJUnit でドライバを指定できるようにしたい。
このボタンからドライバ指定してテストしたい。
f:id:duck8823:20160813144405p:plain

ていうことでちょっと調べたんですが、Spring Bootではプロファイルを切り替える仕組みがもともとある様子。

切り替えるには、application.yml か application.properties かで変わるのですが、 application.properties を使っている場合は、
application-[プロファイル名].properties という名前でファイルを作成し、上書きしたいプロパティを記述すれば良いようです。
以下のファイルを作成しました。

application-phantomjs.properties

# PhantomJSのexeファイルのパス
phantomjs.binary.path=/path/to/pahtnomjs.exe
# Selenideで使用するブラウザ
selenide.browser=phantomjs

IntelliJJUnit 実行時にこのプロパティファイルで上書きするには、 Run/Debug Configuration の VM options に -Dspring.profiles.active=プロファイル名 とすればよいです。
f:id:duck8823:20160813145830p:plain

-Dspring.profiles.active オプションの有と無で設定を作っておけば、選択するだけでドライバを切り替えられます。
リポジトリにプッシュする前にローカルの phantomjs でもテストしておけば、安心安心。

Redmineプラグインを作ってみた

Redmine プラグイン

業務における運用作業をRedmineで管理することになったのですが、そのままでは非常に使いにくい気がしたのでプラグインを作成することにしました。

運用作業をチケットとして管理するには

  • チケットを作成する
  • チケットの進捗率を変える
  • チケットのステータスを変える
  • チケットの作業時間を記録

これらのコストがかかります。
運用作業は「やった」か「やってない」かだと思うので、チケットの進捗率・ステータス・作業時間は自動的に更新したい。
チケットの作成も、同じものをいくつも登録しなければならないので、これを簡単に行えるようにしたいです。

これまで Rails でアプリを作ったことはおろか、 Rubyスクリプトを書いたこともないですが、作ってみました。
作成には以下の記事がとても参考になりました。

Redmine3.2プラグイン開発入門 (1) - 新規画面を追加する - Qiita
Rails を知らない人のための Redmine プラグイン開発ガイド

作成したプラグイン

作成したプラグインGitHubにおいてます。
github.com

インストール

git clone https://github.com/duck8823/redmine_operations.git plugins/redmine_operations
rake redmine:plugins:migrate RAILS_ENV=production

使い方

プロジェクトの設定で使用するモジュールにチェックを入れると、プロジェクトメニューに 「運用」 が追加されます。
「運用」メニューでは、運用作業の登録・編集と、スケジュールの登録が行えます。
各種設定もここで行います。
f:id:duck8823:20160813010558p:plain
スケジュールの登録はカレンダーをポチポチクリックするだけなので、楽だと思います。

ここで作成したチケットには、運用の項目が追加されます。
メニューで登録したタスク一覧が表示され、チェックボックスのチェックの数に連動して進捗率・ステータス・作業時間が自動的に更新されます。
f:id:duck8823:20160813011103p:plain

Redmineプラグイン作成用のコマンドも用意されていて、他言語の開発経験があれば結構簡単に作成できました。
どんどん作成して楽できるところは楽したいですね。

参考にしたサイト

Redmine3.2プラグイン開発入門 (1) - 新規画面を追加する - Qiita
Rails を知らない人のための Redmine プラグイン開発ガイド
jQueryUIのDatepickerで複数の日付を選択する(簡易版) - thinking now...

Bash on Ubuntu on Windows でアレコレ

Windows

8月1日、僕はとても楽しみにしていました。それは遠足の前日の小学生のよう。
8月2日のWindows Anniversary Update で WindowsBash が使えるようになるんです。

8月2日は何度もアップデートを確認してしましました。ポケモンGoの公開前みたいに。
そう、日本時間では8月3日なんですよね。

8月3日の早朝に無事ゲットしました。
Bash on Ubuntu on Windowsのインストール方法とか、ちょっと使ってみたりします。

Bash on Ubuntu on Windowsをインストールする

Bash on Ubuntu on Windowsを利用するには、Windows10を「開発者モード」にする必要があります。[スタートボタン] - [設定] – [開発者向け] から、「開発者モード」を選択してください。
f:id:duck8823:20160805032217p:plain

変更後、再起動する必要がありますので、起動しているアプリケーションを終了してから再起動しましょう。次に、[スタートボタンを右クリック] – [コントロールパネル] – [プログラム] – [Windows機能の有効化または無効化] で [Windows Subsystem for Linux (Beta)]にチェックを入れ再起動してください。
f:id:duck8823:20160805032338p:plain

再起動後、コマンドプロンプトを起動して、bashと入力しエンターキーを押します。ライセンスに同意するか聞かれますのでy を入力しエンターキーを押すことでインストールが開始されます。
f:id:duck8823:20160805032541p:plain

インストール時にBash on Ubuntu on Windowsで利用するユーザ名とパスワードが質問されるので入力しましょう。
インストール後は、コマンドプロンプトからbashと入力しエンターキーを押すか、スタートメニューからBash on Ubuntu on Windowsをクリックすることで起動することができます。

試しにtopコマンドをうってみます。
f:id:duck8823:20160805033321p:plain

ソフトウェアのインストール(apt-get)

Ubuntuでパッケージ管理は apt 系コマンドで行います。c++コンパイラであるg++をインストールするには

sudo apt-get install -y g++

f:id:duck8823:20160805033113p:plain

ソフトウェアのインストール(ソースからコンパイル

例えば、RNA-seqのマッピングに利用されるTopHatをインストールしてみましょう。(筆者は実はバイオ系出身です。)

curl -XGET https://ccb.jhu.edu/software/tophat/downloads/tophat-2.1.1.tar.gz > tophat-2.1.1.tar.gz
tar xvzf tophat-2.1.1.tar.gz
cd tophat-2.1.1
sudo ./configure
sudo make
sudo make install

パスワードを尋ねられた場合は、Bash on Ubuntu on Windowsのインストール時に指定したパスワードを入力してください。インストール後、コマンドライン上で tophatと入力してエンターを押し、以下のように表示されたらインストールは成功です。
f:id:duck8823:20160805033056p:plain

なお、Bash on Ubuntu on WindowsWindows上のファイルにアクセスする場合は少し気を付けなければなりません。ドライブ名が D: の場合は /mnt/d となり、\/ になります。例えば、
D:\Downloads\tophat-2.1.1.tar.gz
は、 Bash on Ubuntu on Windows にアクセスする場合は
/mnt/d/Downloads/tophat-2.1.1.tar.gz
となります。

IntelliJ IDEAのTerminalプラグインBashを使う

開発マシンはWindowsで、公開サーバはLinuxです。IntelliJ IDEAのTerminalプラグインLinuxコマンドを使えるようにします。
[File] - [Settings] - [Tools] - [Terminal] の [Shell path] に bash.exe を指定します。
f:id:duck8823:20160805034136p:plain

設定後、新しいタブを開いてbashが起動していれば成功です。
f:id:duck8823:20160805034313p:plain

vimも起動できます。
f:id:duck8823:20160805034444j:plain

Re:ゼロから始めるフロントエンド開発

JavaScript フロントエンド React

約3週間前・・・。

てことで、フロントエンドの開発に取り掛かりました。
これまでのフロントの開発経験は、

  • テンプレートエンジン(JSP、Velocity)
  • jQuery
  • CSS

StrutsTilesカスタムタグ でコピペを最小限には抑える努力はしていたのですが、ずっとHTMLの部品をコンポーネント化したいと思っていました。

今携わっている案件ではサーバサイドのフレームワークやテスト、CIやGitなどのインフラも一新しているので、フロントエンドの開発環境も整えさせてくれとおねだり。
最初はコンポーネント化を目標としてReactに目をつけ、そこから広げて行きました。

実現したいこととそれを実現するフレームワークなどは以下の通り

これまでフロントエンドは無知だったので、テスティングフレームワークとか全く知りません。
邪道な組み合わせかもしれません。


これらを組み合わせた開発環境と開発の進め方について記録しておきます。

React + ES6 + Flux でコンポーネントを作る

ReactとFluxを組み合わせた構造とデータフローの概要はこちら
f:id:duck8823:20160630204607p:plain

ざっくり自分的にまとめると

  • ActionCreators(Action)
    • Viewからのアクションを受け取る、外部APIとの接続
    • Dispatcherにアクションを渡す
  • Dispatcher
    • アクションを受け取って、コールバックを実行
  • Store
  • React Views(View)
    • データの表示、UIの動作に対するアクションの発行

f:id:duck8823:20160702103222p:plain
APIとの関係はこんな感じかな。
矢印の色は、依存元を示します。
ViewStoreActionに依存しており、子コンポーネント以外からは依存されていません。
多分ここがポイントで、依存されてないからViewの分離がしやすいんだと思います。
renderするViewを変えるだけで簡単に見た目だけ変えたり、分離したViewから同じStoreActionに依存させることで、異なるView同士の連携ができます。

今回作るコンポーネント

今回作りたいのは、ショッピングカートと注文ボタンのようなもの。
f:id:duck8823:20160630202459g:plain

商品ごとに注文ボタンがあって、注文ボタンをクリックすると対応する商品名がカートに追加され、取り消しボタンに変わります。
取り消しボタンを押したらカートから商品が消え、注文ボタンに戻ります。

ディレクトリ構造はこんな感じ。
f:id:duck8823:20160630211952p:plain

どのクラスから書き始めるのが良いかってことになるんですが、
上記の図を見るとDispatcherは他のクラスのAPIに依存していないので、そこから書き始めるのがよさそう。
Dispatcher -> StoreAction -> View の順番で作るのがファイルの行ったり来たりは少なくなりそうです。
※コールバックの順番を制御したい場合はStoreに依存することになりますが、その場合も小さいDispatcherを作ってから複数のStoreを作り、Dispatcherを大きくするのがよいと思いました。

Dispatcher

app-dispatcher.js

import {Dispatcher} from 'flux';
export default new Dispatcher;

シンプルにそのまま使ってます。コールバックを制御したい場合は拡張していきます。

ActionCreators(Action)

Dispatcher.dispath()

static add(item) {
    AppDispatcher.dispatch({
        type: Constants.ADD,
        item: item
    });
}

staticメソッドの中で、アクションをDispatcherに渡しています。
Storeの中で処理を振り分けるために、typeを渡しています。
あとは、Storeで保存などするためのデータ。ここではitemとしてViewから渡される商品オブジェクトになります。

cart-action.js

import AppDispatcher from "../../app-dispatcher";
import {Constants} from "./constants";

/**
 * カートのAction
 */
export default class CartAction {
    
    static add(item) {
        AppDispatcher.dispatch({
            type: Constants.ADD,
            item: item
        });
    }

    static remove(item) {
        AppDispatcher.dispatch({
            type: Constants.REMOVE,
            item: item
        });
    }
}

クラス全体。ここでは、2種類のActionをDispatcherに渡してます。
今回はシンプルなものですが、外部APIと接続する場合はここで実行します。

Store

Dispatcher.register()

constructor() {
    super();
    ...
    AppDispatcher.register(this._onAction.bind(this));
}

Dispatcherからアクションを受け取るために、コンストラクタでDispatcherにアクションを登録します。
ここで登録したアクションで、処理を振り分けています。


EventEmitter.emit()

 add(item) {
    ...
    this.emit(Constants.ADD);
}

Node.jsのEventEmitterを利用し、emit()でViewに変更があったことを通知します。


cart-store.js

import EventEmitter from 'events';
import AppDispatcher from '../../app-dispatcher';
import {Constants} from './constants';

const CONSTANTS = {
    CART : 'CART'
};
/**
 * カートのStore
 */
class CartStore extends EventEmitter {

    constructor() {
        super();
        var items = window.sessionStorage.getItem(CONSTANTS.CART);
        this._items = items ? JSON.parse(items) : {};
        AppDispatcher.register(this._onAction.bind(this));
    }

    /**
    * 商品を追加する
    * @param item
    */
    add(item) {
        if(this.contains(item)){
            throw new Error("already contains: " + item.id);
        }
        this._items[item.id] = item;
        this._save();
        this.emit(Constants.ADD);
    }

    /**
    * 商品を削除する
    * @param item
    */
    remove(item) {
        if(!this.contains(item)){
            throw new Error("not contains: " + item.id);
        }
        delete this._items[item.id];
        this._save();
        this.emit(Constants.REMOVE);
    }

    /**
    * idが含まれているかどうか
    * @param item
    */
    contains(item) {
        return this._items[item.id] ? true : false;
    };

    /**
    * WebStorageに保存する
    * @private
    */
    _save() {
        window.sessionStorage.setItem(CONSTANTS.CART, JSON.stringify(this._items));
    }

    /**
    * 初期化する
    * @private
    */
    _clear() {
        this._items = {};
        window.sessionStorage.removeItem(CONSTANTS.CART);
    }

    _onAction(action) {
        switch (action.type) {
            case Constants.ADD:
                this.add(action.item);
                break;
            case Constants.REMOVE:
                this.remove(action.item);
                break;
            default:
            // nothing to do.
        }
    }

    getItems() {
        var items = [];
        Object.keys(this._items).forEach((key) => items.push(this._items[key]));
        return items;
    }
}
const cartStore = new CartStore();
export default cartStore;

クラス全体です。EventEmitterを継承しています。
最後にインスタンスを生成し、そのインスタンスをexportします。

View

Store.getter()

constructor(props) {
    super(props);
    this.state = {
        items: CartStore.getItems()
    };
}

コンストラクタ内でStoreから値を取得し、Viewコンポーネントの初期値とします。


EventEmitter.on()

componentWillMount() {
    CartStore.on(Constants.ADD, () => {
        this.setState({
            items: CartStore.getItems()
        });
    });
    ...
}

componentWillMountでコンポーネントがマウントされる際(DOMツリーに追加される前)に実行する処理を登録することができます。 EventEmitterを利用してここではConstants.ADDのコールバックにsetState()を指定しています。


Action.do

render() {
   ...
   <div className="ui vertical animated button" ref="button" onClick={this._onAction.bind(this)}>
   ...
}

render()内でボタン部分にonClickを登録しています。

_onAction() {
    const {contains} = this.state;
    const {item} = this.props;
    if(contains){
        CartAction.remove(item);
    } else {
        CartAction.add(item);
    }
    this.setState({contains: contains ? false : true});
}

クリックした際に呼ばれるメソッド内で、Actionのstaticメソッドを実行します。

カートのView

cart-view.js

import React from "react";
import CartStore from "./cart-store";

import {Constants} from "./constants";

import $ from 'jquery';
import 'semantic-ui-modal/modal.css'
$.fn.modal = require('semantic-ui-modal');

import 'semantic-ui-dimmer/dimmer.css';
$.fn.dimmer = require('semantic-ui-dimmer');

import 'semantic-ui-transition/transition.css';
$.fn.transition = require('semantic-ui-transition');

/**
 * カートのView
 */
export default class CartView extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            items: CartStore.getItems()
        };
    }

    componentWillMount() {
        CartStore.on(Constants.ADD, () => {
            this.setState({
                items: CartStore.getItems()
            });
        });
        CartStore.on(Constants.REMOVE, () => {
            this.setState({
                items: CartStore.getItems()
            });
        });
    }

    _open() {
        $(".ui.modal.cart").modal('show');
    }

    render() {
        return (
            <div>
                <div className="ui labeled button modal-button" ref="modal" onClick={this._open.bind(this)}>
                    <div className="ui button">
                        <i className="shop icon"></i> Cart
                    </div>
                    <div className="ui basic left pointing label count">
                        {this.state.items.length}
                    </div>
                </div>
                <div className="ui modal cart">
                    <i className="close icon"></i>
                    <div className="header">
                        Cart
                    </div>
                    <div className="ui description">
                        <div ref="item-list" className="ui middle aligned divided list item-list">
                            {this.state.items.map((item) => {
                                return (
                                    <div key={item.id} className="item">
                                        <div className="content">
                                            {item.name}
                                        </div>
                                    </div>
                                )
                            })}
                        </div>
                    </div>
                    <div className="actions">
                        <div className="ui black deny button">
                            Cancel
                        </div>
                        <div className="ui positive right labeled icon button">
                            OK
                            <i className="checkmark icon"></i>
                        </div>
                    </div>
                </div>
            </div>
        )
    }
}
アクションを実行する注文ボタンのView

cart-button.js

import React from 'react';

import CartStore from './cart-store';
import CartAction from './cart-action';

/**
 * ボタンのView
 */
export default class CartButton extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            contains: CartStore.contains(this.props.item)
        };
        
    }

    _onAction() {
        const {contains} = this.state;
        const {item} = this.props;
        if(contains){
            CartAction.remove(item);
        } else {
            CartAction.add(item);
        }
        this.setState({contains: contains ? false : true});
    }

    render() {
        const {contains} = this.state;
        return (
            <div className="ui vertical animated button" ref="button" onClick={this._onAction.bind(this)}>
                <div className="hidden content">
                    {contains ? 'Remove' : 'Add'}
                </div>
                <div className="visible content compact">
                    <i className={contains ? "trash outline icon" : "add to cart icon"}></i>
                </div>
            </div>
        )
    }
}

ViewでjQueryCSSフレームワークを使う

カートのViewではjQuerySemantic UIを利用しています。
jQueryは嫌われているけど、やっぱり便利ですし、デザインセンスが皆無なのでCSSフレームワークに頼りたい。

ES6でjQuery$として扱いたい場合は、

import $ from 'jquery';

cssを扱いたい場合も他のjavascriptと同じようにimport文が使えます。便利。

import 'semantic-ui-modal/modal.css'

この modal のようにCSSフレームワークjQueryの機能をオーバーライドしている場合は、

$.fn.modal = require('semantic-ui-modal');

このように書くことで実現できました。

Webpackによるビルド

ES6はブラウザによって対応状況が異なるので、WebpackBabelを指定してES5形式に変換(ビルド)します。
webpack.conf.js

var webpack = require('webpack');
var path = require('path');

var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    entry: {
        bundle: './path/to/src/entry.js'
    },
    output: {
        path: path.join(__dirname, 'path/to/dist'),
        filename: 'bundle.js'
    },
    plugins: [
        new ExtractTextPlugin('bundle.css')
    ],
    module : {
        loaders: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                loader: 'babel'
            },
            {
                test: /\.css(\?.+)?$/,
                loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
            },
            {
                test : /\.(svg|woff2?|eot|ttf|png|gif)$/,
                loader: 'url'
            }
        ]
    }
};

ローダー

ファイルに対してどのようにコンパイルするかを指定します。

loaders: [
    {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel'
    },
    {
        test: /\.css(\?.+)?$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
    },
    {
        test : /\.(svg|woff2?|eot|ttf|png|gif)$/,
        loader: 'url'
    }
]

testで指定した正規表現にマッチするファイル対して、loaderを指定します。
js(x)ファイルはbabel(ES6をES5に変換)
cssファイルはExtractTextPlugin.extract('style-loader', 'css-loader')(実ファイルも作るプラグイン
フォントや画像などはurlbase64エンコード

babelはオプションを.babelrcで指定することができます。
queryとして記述できるのですが、テストランナーでも同じオプションを利用したいのでファイルとして置いておくと便利です。

.babelrc

{
  "presets": ["es2015", "react"]
}

CSSのファイル出力

CSSはテンプレートエンジンでも利用したいので、ファイルとして出力します。
ExtractTextPluginを利用します。

var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    ...
    plugins: [
        new ExtractTextPlugin('bundle.css')
    ],
    module : {
        loaders: [
            ...
            {
                test: /\.css(\?.+)?$/,
                loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
            },
        ]
    }
}

pluginsで指定したファイル名bundle.cssに出力されます。

出力先

output

output: {
    path: path.join(__dirname, 'path/to/dist'),
    filename: 'bundle.js'
},

ビルドの起点

entry

entry: {
    bundle: './path/to/src/entry.js'
}

起点となるスクリプトを指定します。
このスクリプトを起点として依存されていないファイルはビルドされないので注意してください。

entry.js

import 'semantic-ui-css/semantic.css'

import React from "react";
import ReactDOM from "react-dom";

import $ from "jquery";

import Cart from "./components/cart/cart-view";
import Button from "./components/cart/cart-button-view";

$(".ui.cart").each(function(){
    ReactDOM.render(
        <Cart />,
        $(this).get(0)
    );
});
$(".ui.cart-button").each(function(){
    var item = {
        id : $(this).data('id'),
        name :$(this).data('name')
    };
    ReactDOM.render(
        <Button item={item} />,
        $(this).get(0)
    );
});

テンプレートエンジンで利用するCSSはここで指定します。
また、自分はCSSフレームワークのようにセレクタコンポーネントレンダリングしたいので、

$(".ui.cart").each(function(){
    ReactDOM.render(
        <Cart />,
        $(this).get(0)
    );
});

て書いてます。これでhtmlでは

<div class="ui cart" ></div>

レンダリングしてくれます。

ビルドの実行

ビルドの実行はコマンドラインから

node_components\.bin\webpack

でビルドを実行します。
webpackはpackage.jsonで依存関係に含まれているので、npm installでインストールできます。

テスト

テストを書きましょう。テストは心を豊かにします。
ES6のテストができて、カバレッジ算出を目指しました。

スティングフレームワーク

Mocha
情報量が多かったので選びました。
describeitでテストケースを区切っていきます。
beforeEach()で各テストケースの前に実行する処理を記述できます。

beforeEach(()=>{
    ...
});

Assertionライブラリ

Chai
これも情報量が多かったので選びました。
色々用意されているみたい。

配列の要素数

expect(cart.state.items).to.have.length(1);

Errorをthrowしているかどうか。

expect(()=>CartAction.add({
        id: '1',
        name: '1'
    })).to.throw(Error);

作成したテスト

import React from "react";
import ReactTestUtils from "react-addons-test-utils";
import {expect} from "chai";

import Cart from "../src/components/cart/cart-view";
import CartButton from "../src/components/cart/cart-button-view";
import CartStore from "../src/components/cart/cart-store";
import CartAction from "../src/components/cart/cart-action";

/**
 * カートコンポーネントのテスト
 */
describe('Cart', ()=>{

    beforeEach(()=>{
        CartStore._clear();
    });

    it('カートの中身は初期値は0件である', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);

        expect(component.state.items).to.have.length(0);
    });

    it('カートに追加すると1件になる', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);
        CartAction.add({
            id  : '1',
            name: '1'
        });

        expect(component.state.items).to.have.length(1);
    });

    it('カートに3件追加し、1件削除すると2件になる', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);
        CartAction.add({
            id  : '1',
            name: '1'
        });
        CartAction.add({
            id  : '2',
            name: '2'
        });
        CartAction.add({
            id  : '3',
            name: '3'
        });
        CartAction.remove({
            id  : '2',
            name: '2'
        });
        expect(component.state.items).to.have.length(2);
    });

    it('既に含まれているIDを追加しようとするとエラーになって追加されない', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);
        CartAction.add({
            id: '1',
            name: '1'
        });
        expect(component.state.items).to.have.length(1);

        expect(()=>CartAction.add({
            id: '1',
            name: '1'
        })).to.throw(Error);

        expect(component.state.items).to.have.length(1);
    });

    it('含まれていないIDを削除しようとするとエラーになる', () => {
        let component = ReactTestUtils.renderIntoDocument(<Cart />);
        CartAction.add({
            id: '1',
            name: '1'
        });
        expect(component.state.items).to.have.length(1);

        expect(()=>CartAction.remove({
            id: '2',
            name: '2'
        })).to.throw(Error);

        expect(component.state.items).to.have.length(1);
    });

    it('リクエストボタンをクリックしたらカートに追加される', ()=>{
        let cart = ReactTestUtils.renderIntoDocument(<Cart />);
        let cartButton = ReactTestUtils.renderIntoDocument(
            <CartButton item={{id : '1', name : '1'}} />
        );
        ReactTestUtils.Simulate.click(cartButton.refs['button']);

        expect(cart.state.items).to.have.length(1);
    });

    it('既に含まれている商品のリクエストボタンをクリックしたらカートから削除される', ()=>{
        var item = {id : '1', name : '1'};
        CartAction.add(item);

        let cart = ReactTestUtils.renderIntoDocument(<Cart />);
        expect(cart.state.items).to.have.length(1);

        let cartButton = ReactTestUtils.renderIntoDocument(
            <CartButton item={item} />
        );
        ReactTestUtils.Simulate.click(cartButton.refs['button']);

        expect(cart.state.items).to.have.length(0);
    });
});


テストランナー

ES6のテストを実行するにはやはりBabelでビルドする必要があります。
ビルドしてテストを実行するためにテストランナーであるKarmaを用いました。

Karmaはkarma.conf.jsで設定します。

ビルド前のソースコードカバレッジ計算

カバレッジ計算ではistanbu-instrumenter-loaderlが使われているようです。
この場合だとwebpackでビルドしたコードに対してカバレッジを計算してしまうので、isparta-loaderを使うといいようです。

出来上がったkarma.conf.js

karma.conf.js

var path = require('path');

module.exports = function(config) {
    config.set({
        basePath: '',
        frameworks: ['phantomjs-shim','mocha'],
        files: [
            'path/to/test/**/*.spec.js'
        ],
        exclude: [
        ],
        plugins: [
            'karma-phantomjs-launcher',
            'karma-phantomjs-shim',
            'karma-mocha',
            'karma-sourcemap-loader',
            'karma-webpack',
            'karma-coverage',
            'karma-mocha-reporter'
        ],
        preprocessors: {
            'path/to/test/**/*.spec.js': ['webpack', "sourcemap"]
        },
        webpack: {
            devtools: 'inline-source-map',
            isparta: {
                embedSource: true,
                noAutoWrap: true
            },
            module: {
                loaders: [
                    {
                        test: /\.jsx?$/,
                        exclude: /node_modules/,
                        loader: 'babel'
                    },
                    {
                        test: /\.css(\?.+)?$/,
                        loader: 'style!css'
                    }
                ],
                preLoaders: [
                    {
                        test: /\.jsx?$/,
                        exclude: [
                            path.resolve('path/to/test/'),
                            path.resolve('node_modules/')
                        ],
                        loader: 'isparta'
                    }
                ]
            }
        },
        webpackMiddleware: {
            noInfo: true
        },
        port: 9876,
        browsers: ['PhantomJS'],
        reporters: ['mocha', 'coverage'],
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: false,
        singleRun: true,
        concurrency: Infinity,
        coverageReporter: {
            dir: 'path/to/test/report/',
            reporters: [
                { type: 'text' },
                { type: 'html', subdir: '.' }
            ]
        }
    })
};

ここで使うbabelの設定は上述の.babelrcで行われるので、ちょっとスッキリしています。

テストの実行は

karma start

f:id:duck8823:20160701005751p:plain
カバレッジを計算してくれました。

レポートでhtmlを指定した場合、どの部分をカバーできていないか教えてくれます。
f:id:duck8823:20160701012639p:plain

また、package.json

"scripts": {
    "test": "karma start"
  }

と設定しているので

npm test

でも実行できます。

フロントエンド開発をちゃんとやろうと思って構築を始めたのですが、こんなにも開発環境は充実していたんですね。
jQueryで頑張っていたのが嘘のよう。

今回作成したコンポーネントGitHubに置いておきました。 github.com

フロントエンドは変化が激しいといわれてますが、それぞれ目的があって見つけてきたものなので、目的が変わらなければこのままでもいいかなと思ったりします。