超お久しぶりです。Gehirn News ライター、もといエンジニアの KOBA789 です。毎度書き始めが「お久しぶりです」になってしまうこの怠慢さ、どうかお許しいただきたく(拝承メソッド)。

さて、最近社内では Go 言語が(自分と隣席の @yosida95 の間で)ブームです。速いコンパイルと最小限の言語仕様、割り切りの潔さと型の便利さに魅了され、毎日 Go 言語のコードを書いています。

実務で使うとなればテストは必須です。いくら型があるとはいえ、Haskell ほど強力な型システムを持っているわけではなく、副作用もあるので QuickCheck なども難しく、コード証明ができるような言語でもありません(逆にこのあたりが実務で使う上で適度に現実味があってよい)。

さて、Go 言語では *_test.go という名前のファイルがテストコードとなり、go test コマンドによって実行することができます。しかし、いちいちエディタを閉じて(あるいはサスペンドして)シェルへ戻り、go testとタイプしてテストを実行するのは不便です。バックグラウンドでファイル変更を監視して、保存と同時に実行してくれればいいのに、と思うのが普通でしょう。できればテストの結果が Growl によって通知されるとなおよい、というところです。

今回想定する環境

  • 同一サブネットの中に Mac(デスクトップ)と Ubuntu(SSH ターゲット)がぶらさがってる
  • エディタを動かすのもコードを実行するのも Ubuntu 上
  • でも Growl 通知を表示するのは Mac 上
    • もちろん Growl は導入済み
    • Growl の設定から「ネットワーク」の「外部からの通知を受け付ける」を有効にしている

今回使う主なソフトウェア

ここで、今回の環境を構築するためのソフトウェアについて説明します。

OMake

おまけではない。(←重要)

ビルドシステム Make の多機能版、といえばわかりやすいでしょうか。OMake もビルドシステムの一種です。

OMake の特筆すべき機能はファイルのの変更監視機能。継続的にファイルの変更を監視して、変更があれば自動的にタスクを再実行する、というものです。今回はこれを使って継続テストを実現します。

Growl

言わずと知れた Mac の通知アプリケーション。

OS X Mountain Lion では OS 自体に「通知センター」という通知システムが組み込まれましたが、依然として Growl のほうが多機能で優秀であり、よく使われています。

この Growl にはネットワーク越しで通知を飛ばす機能があります。弊社では Mac をデスクトップ環境として使い、社内の仮想マシンサーバー上に立てた VM の Linux 上へその Mac から SSH して開発をするスタイルを推奨しています。そのため、omakego test 自体は通知を出すべき Mac とは別のマシンで実行されています。しかし、この Growl のネットワーク通知機能によって、別マシンへ通知を飛ばすことが可能なので、そのような環境でも問題なく通知を表示することができます。

構築

では実際に構築を行います。以下のコマンドはすべて Ubuntu 上で実行します。

Growl通知用コマンドの準備

まずは、Growl 通知のためのコマンドを作ります。Growl 通知なくても継続テストだけできればいいよ、って人は読み飛ばして OK です。

Growl 通知用のコマンドは Python で作ります。Python 2.7 の環境があればよいです。ない場合は適当に用意してください。

まず、Growl 通知用ライブラリである gntp をインストールします。

$ sudo easy_install gntp

pip とかでもいい気がしますが私は Python 詳しくないのでわかりません。ぶっちゃけ、GNTP 喋れれば Ruby でも Node.js でもよいです。

実際のコマンドのソースは以下のようになります。前述の通り、由緒正しき Pythonista でもなんでもないので適当です。YOUR_MAC_IP は Mac の IP アドレスに書き換えてください。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys

from gntp import notifier

def main():
    growl = notifier.GrowlNotifier(notifications=['default'],
                                   defaultNotifications=['default'],
                                   hostname='YOUR_MAC_IP')
    growl.register()

    desc = 'Failed.'
    image = 'https://raw.github.com/visionmedia/mocha/master/images/error.png'

    if sys.argv[1] == '0':
        desc = 'Passed.'
        image = 'https://raw.github.com/visionmedia/mocha/master/images/ok.png'

    growl.notify(noteType='default', title='CI Test', description=desc,
                 icon=image, sticky=False, priority=1)

if __name__ == '__main__':
    main()

これを growl-notify として適当な場所に保存、chmox +x で実行権限を与えてからパスを通してください。

パスが通れば、以下のようにしてデスクトップに通知を表示できます。

$ growl-notify 0 # says "Passed." with the ok icon
$ growl-notify 1 # says "Failed." with the error icon

ソースコードとテストコードの準備

omake で継続テストを行うソースコードやテストコードの準備をしましょう。

以下のようなファイル構成を想定します。

$ ls
add.go      add_test.go

単純ですね。それぞれのファイルの内容は以下のとおりです。

package calc

func Add (a int, b int) int {
    return a + b
}
package calc

テストコードはとりあえず空っぽです。試しにこのディレクトリで go test を実行してみます。

$ go test
testing: warning: no tests to run
PASS
ok      _/Users/koba789/src/calc    0.025s

空のテストは waring を吐きつつも PASS します。よい感じです。

OMakeのセットアップ

ではいよいよここに omake をセットアップします。

まずは omake をインストールしなくてはなりません。 ファイル変更監視を行う fam というソフトウェアも共にインストールします。

$ sudo apt-get install omake fam

omake のインストールが完了したら、プロジェクトディレクトリに omake 関連のファイルを生成します。

$ omake --install
$ ls
OMakefile   OMakeroot   add.go      add_test.go

今回は特に難しいことはしないので、OMakeroot は特にいじる必要はありません。OMakefile だけ編集します。OMakefile は以下のようにします。デフォルトでたくさんのコメントが書かれていますが、すべて消してしまって問題ないです。

SRC_GO = $(glob *.go)

.PHONY: test

test: $(SRC_GO)
    go test && growl-notify 0 || growl-notify 1

ここで重要なのは、omake 組み込みの glob 関数です。ワイルドカードを渡すことでファイル/ディレクトリを列挙することができます。なので、OMakefile 中にターゲットのファイルリストを埋め込む必要はありません。自動的にディレクトリ内のすべての *.go ファイルを対象とすることができます。便利ですね!

利用

ではいよいよ、使ってみます。

プロジェクトディレクトリで以下のコマンドを実行します。

$ omake test -P

一度だけテストが実行され、通知で Passed. と表示された後、コマンドがブロックしてファイル変更監視状態になれば成功です。

この状態で tmux などで新しくターミナルを開き、そちらでエディタをたちあげて add_test.go を以下のように編集して、保存してみましょう。

package calc

import "testing"

func TestAdd (t *testing.T) {
    t.Error("failed")
}

これは確実にコケるテストです。保存した瞬間に通知で Failed. と表示されたと思います。

このテストコードをまともなテストに書き直しましょう。以下のようにします。

package calc

import "testing"

func TestAdd (t *testing.T) {
    sum := Add(2, 3)
    if sum != 5 {
        t.Error("failed: 2 + 3 != 5")
    }
}

上記のコードを保存した瞬間、同様に Passed. と表示されたことが確認出来れば OK です。

ここで実装の方をわざと間違えてみましょう。

package calc

func Add (a int, b int) int {
    return a + b + 1
}

うっかり1大きい値を返すようにしました。こうすると、テストがコケるようになったと思います。確認できたら、戻しておきましょう。

以上で基本的な使い方の説明は終わりです。詳しくは omake の日本語マニュアルなどを参照してください。

1. ガイド — OMakeマニュアル 日本語訳

まとめ

いかがだったでしょうか。omake を使うことで、とても簡単に継続テストを行えるのだということが伝われば幸いです。

Ruby の guard や Node.js の mocha でも同様のことができますが、それらは言語依存、フレームワーク依存で汎用性に欠けます。一方 omake はとても汎用的なビルドシステムなので、ファイル変更を追跡してタスクを実行するという処理をどんな対象(今回は Go 言語のテスト)に対しても記述することができます。

ビルドシステムを用いてタスクを記述することは、保守性の向上や各種 CI ツールとの連携の容易にすることにもつながりますので、積極的に行なって行きたいものです。そこで汎用的な omake を用いて記述すれば、複数の言語が乗り入れるようなプロジェクトであっても統一的にテストを実行することができるので、より一層便利になるかと思います。