YAMAGUCHI::weblog

噛み付き地蔵に憧れて、この神の世界にやってきました。マドンナみたいな男の子、コッペです。

EUnitとOMakeでテスト駆動開発

はじめに

こんにちは、セコムしてますか?僕はしてません。さて、Erlangでちょこちょこと質問してたら、id:kuenishiから「てめえ、ぐだぐだ言う前にまずOMakeとEUnit使ってひたすらテストしろや」と言われたのでそのセットアップなどをしました。

参照

書いたもの

とりあえずテンプレートとして使えるように置いておきますね。更新出来るところから更新します。

設定

まずはコードを書く

テスト以前にそもそものコードを書きますよね。というわけで今回は「決められた値までの素数列を求める関数primesを実装する」ということで書いてみました。

bottom(List)->
    case List of
        [] -> [];
        [X] -> X;
        [_|Xs] -> bottom(Xs)
    end.


sift(Divider, Nums)->
    lists:filter(fun(X) -> X rem Divider =/= 0 end, Nums).


sieve(Nums)->
    sieve([], Nums).
sieve(Accu, Nums)->
    case {Accu, Nums} of
        {_, []} ->
            Accu;
        {[], _} ->
            sieve([hd(Nums)], sift(hd(Nums), tl(Nums)));
        {_, [X|Xs]} ->
            B = bottom(Xs),
            if 
               X*X > B ->
                    lists:reverse(Accu, Nums);
               true ->
                    sieve([X|Accu], sift(X, Xs))
            end
    end.

primes(Limit)->
    sieve(lists:seq(2, Limit)).
テストコードを書く

さて、ちゃんと上のコードが動くのかっつーわけで、「並行して」テストコードを書いていきますよ。並行して、っていうか関数名書いたら実装始める前にテスト書きますよね。期待する結果を書いて。でこんな感じでテスト書きました。

bottom_test_()->
    [?_assert( [] =:= bottom([]) ),
     ?_assert( 1 =:= bottom([1]) ),
     ?_assert( 10 =:= bottom(lists:seq(1, 10)) ),
     ?_assert( d =:= bottom([a, b, c, d]) )
    ].

sift_test_()->
    [?_assert( [] =:= sift(2, []) ),
     ?_assert( [] =:= sift(1, lists:seq(1, 5)) ),
     ?_assert( lists:seq(1, 9, 2) =:= sift(2, lists:seq(1, 10)) )
    ].

sieve_test_()->
    [?_assert( [] =:= sieve([]) ),
     ?_assert( [1] =:= sieve(lists:seq(1,10)) ),
     ?_assert( [2,3,5,7] =:= sieve(lists:seq(2,10)) )
    ].

primes_test_()->
    [?_assert( [2] =:= primes(2) ),
     ?_assert( [2,3,5,7] =:= primes(10) )
    ].

_test_()で終わる関数を書いておくと、EUnitはそれをテストジェネレータとして認識して全部実行してくれます。すごいですねー。

コンパイルと実行

さて、コード書いただけではクソの役にも立たないのでコンパイルして実行します。上の2つを全部まとめたのがこれ。

%%%-----------------------------------------------------------------------------
%%% @author Yoshifumi YAMAGUCHI <ymotongpoo AT gmail.com>
%%% @copyright (C) 2010, Yoshifumi YAMAGUCHI
%%% @doc
%%%
%%% @end
%%% Created : 23 Oct 2010 by Yoshifumi YAMAGUCHI <ymotongpoo AT gmail.com>
%%%-----------------------------------------------------------------------------

-module(sample).
-define(EUNIT, true)
-export([bottom/1, sift/2, sieve/1, primes/1]).
-compile(export_all).

%%%-----------------------------------------------------------------------------

bottom(List)->
    case List of
        [] -> [];
        [X] -> X;
        [_|Xs] -> bottom(Xs)
    end.


sift(Divider, Nums)->
    lists:filter(fun(X) -> X rem Divider =/= 0 end, Nums).


sieve(Nums)->
    sieve([], Nums).
sieve(Accu, Nums)->
    case {Accu, Nums} of
        {_, []} ->
            Accu;
        {[], _} ->
            sieve([hd(Nums)], sift(hd(Nums), tl(Nums)));
        {_, [X|Xs]} ->
            B = bottom(Xs),
            if 
               X*X > B ->
                    lists:reverse(Accu, Nums);
               true ->
                    sieve([X|Accu], sift(X, Xs))
            end
    end.

primes(Limit)->
    sieve(lists:seq(2, Limit)).


%%%-----------------------------------------------------------------------------

-ifdef(EUNIT).
-include_lib("eunit/include/eunit.hrl").

bottom_test_()->
    [?_assert( [] =:= bottom([]) ),
     ?_assert( 1 =:= bottom([1]) ),
     ?_assert( 10 =:= bottom(lists:seq(1, 10)) ),
     ?_assert( d =:= bottom([a, b, c, d]) )
    ].

sift_test_()->
    [?_assert( [] =:= sift(2, []) ),
     ?_assert( [] =:= sift(1, lists:seq(1, 5)) ),
     ?_assert( lists:seq(1, 9, 2) =:= sift(2, lists:seq(1, 10)) )
    ].

sieve_test_()->
    [?_assert( [] =:= sieve([]) ),
     ?_assert( [1] =:= sieve(lists:seq(1,10)) ),
     ?_assert( [2,3,5,7] =:= sieve(lists:seq(2,10)) )
    ].

primes_test_()->
    [?_assert( [2] =:= primes(2) ),
     ?_assert( [2,3,5,7] =:= primes(10) )
    ].

-endif.

早速コンパイルして実行ですね。EUnitはmというモジュールだったらm:test()という関数として組み込まれるので、これを実行してあげればよいわけです。

$ erlc sample.erl
$ erl -noshell -s sample test -s init stop
  All 12 tests passed.

おお、ちゃんとテスト通りましたね。じゃあわざとこけるようにテストを書き換えてみます。

bottom_test_()->
    [?_assert( [] =:= bottom([]) ),
     ?_assert( 1 =:= bottom([1]) ),
     ?_assert( 1 =:= bottom(lists:seq(1, 10)) ), % ここを10じゃなくて1にしてみました
     ?_assert( d =:= bottom([a, b, c, d]) )
    ].

再度コンパイルして実行。

$ erlc sample.erl 
$ erl -noshell -s sample test -s init stop
sample:59: bottom_test_...*failed*
::error:{assertion_failed,[{module,sample},
                         {line,59},
                         {expression,"1 =:= bottom ( lists : seq ( 1 , 10 ) )"},
                         {expected,true},
                         {value,false}]}
  in function sample:'-bottom_test_/0-fun-4-'/0

=======================================================
  Failed: 1.  Skipped: 0.  Passed: 11.

ちゃんと?こけてくれてますね。他の11個のテストに関しては通りましたよ、って出てます。ここまでで手動でEUnitを走らせるところまでは来ました。あとはOMakeを使って、ファイルを更新するたびにコンパイルとEUnitの実行をしてもらえるようにすればいいだけです!

OMakefileの設定

OMakeはOCamlで実装されてるビルドツールで、OMake言語という専用言語でGNU Makeよりも柔軟な記述が出来ます。さらにomake -Pというオプションを点けて実行すると、Flymakeのようにファイルを更新するたびにビルドが走ります。早速OMakeを書いてみます。詳しいことは上記リンクに任せるとして、今回は次のようなディレクトリ構成にしました。

unittest/
├── OMakefile
├── OMakeroot
└── src
    ├── OMakefile
    └── sample.erl

OMakerootはプロジェクトのルートディレクトリに必ずないといけないファイルで、これを起点としてディレクトリを行ったり来たりできます。OMakefileはプロジェクト内の各ディレクトリに置いておくファイルで、この中でビルドとか書いたりします。
OMakerootは完全にid:kuenishiのやつをパクリましたwのでOMakefileで使ってない設定がたくさんあります。いずれ使います。

  • OMakeroot
########################################################################
# The standard OMakeroot file.
# You will not normally need to modify this file.
# By default, your changes should be placed in the
# OMakefile in this directory.
#
# If you decide to modify this file, note that it uses exactly
# the same syntax as the OMakefile.
#

#
# Include the standard installed configuration files.
# Any of these can be deleted if you are not using them,
# but you probably want to keep the Common file.
#
open build/C
open build/OCaml
open build/LaTeX

# sth like OTP.om
public.ERLC=$(shell which erlc)
public.SRCDIR=src
public.TESTDIR=test
public.EBIN=ebin
public.ERLCFLAGS=-DDEBUG +debug_info -Wall
public.ERL=erl
public.ROOT=$(shell pwd)

public.BEAM_EXT=.beam
public.ERL_EXT=.erl

public.INCLUDES[] = include
public.INCLUDES_OPT = -I
#
# Add the -I option to the includes lazily.
# Don't redefine this variable unless you know what you are doing.
#
public.PREFIXED_INCLUDES = $`(addprefix $(INCLUDES_OPT), $(INCLUDES))

#%$(BEAM_EXT): %$(ERL_EXT) #:scanner: scan-erl-%$(ERL_EXT)
#    $(ERLC) $(ERLCFLAGS) $(PREFIXED_INCLUDES) $<

MakeBeams(names) =
    erl2beam(name) =
        private.beam = $(EBIN)/$(removesuffix $(basename $(name)))$(BEAM_EXT)
        $(beam): $(name)
            $(ERLC) $(ERLCFLAGS) -pa $(EBIN) $(PREFIXED_INCLUDES) -o $(EBIN) $<
#            $(ERL) -pa $(EBIN) -noshell -eval '$(removesuffix $(basename $(name))):test().' -s init stop 
        return $(string $(beam))
    private.beams=$(names.map $(erl2beam))
    return $(beams)

erls=$(glob $(string $(SRCDIR)/*$(ERL_EXT)))
beams=$(MakeBeams $(erls))
apps= #$(glob $(string $(EBIN)/*.app))

#
# The command-line variables are defined *after* the
# standard configuration has been loaded.
#
DefineCommandVars()

#
# Include the OMakefile in this directory.
#
.SUBDIRS: .
  • OMakefile
########################################################################
# Phony targets are scoped, so you probably want to declare them first.
#
.PHONY: all clean

# eprintln( $(string $(beams)) )
testerls=$(glob $(string $(TESTDIR)/*$(ERL_EXT)))

# traverse the subdirs except $(dirs)
Subdirs_except(dirs) =
 # need to export since .SUBDIRS is evaluated in the global scope
 export VISIT_SUBDIRS

 sub_omakefiles = $(glob i, */OMakefile)
 subdirs = $(sub_omakefiles.map $(dirname))

 VISIT_SUBDIRS=$(set-diff $(subdirs), $(dirs))

 # The rule
 .SUBDIRS: $(VISIT_SUBDIRS)

# traverse all the subdirs
Subdirs() =
 Subdirs_except($(array))

Subdirs()
  • src/OMakefile
MODULE = sample
FILES = $(MODULE).erl
TARGET = $(MODULE).beam

.PHONY: all $(TARGET)

.DEFAULT: all

all: $(TARGET)

$(TARGET): $(FILES)
    $(ERLC) $(FILES)
    $(ERL) -noshell -s $(MODULE) test -s init stop

clean:
    rm -f *.beam

これで準備完了。プロジェクトルートでomakeを打ってもいいですし、srcディレクトリ内で打ってもいいです。

$ omake
*** omake: changing directory to /Users/ymotongpoo/src/erlang/unittest
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.02 sec)
- build src <.DEFAULT>
+ erl -noshell -s sample test -s init stop
  All 12 tests passed.
*** omake: done (1.43 sec, 0/0 scans, 1/1 rules, 0/73 digests)      

ちゃんとEUnitが走りました。

omake -Pで幸せ

さていよいよomake -Pで幸せになりたいと思います。今回はtakeN/2というリストの先頭からN個の要素を取ってきたリストを返す関数を追加したいと思います。まずコマンドラインでおもむろにomake -Pを起動させます。

$ omake -P
*** omake: changing directory to /Users/ymotongpoo/src/erlang/omake-eunit-template
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.02 sec)
- build src <sample>
+ erl -noshell -s sample test -s init stop
  All 12 tests passed.
*** omake: done (1.42 sec, 0/0 scans, 1/1 rules, 0/86 digests)                                                             
*** omake: polling for filesystem changes

ここで通常のomakeと違い、最後に一行、ポーリングして監視してますよ、って感じのメッセージが出てると思います。ここでsample.erlに以下のような行を追加して保存します。

takeN(N, List)-> % わざと関数名だけ書いて放置します
...
takeN_test_()-> % テストを書いておきます
    [?_assert( [] =:= takeN(0, lists:seq(1,10)) ),
     ?_assert( [1] =:= takeN(1, lists:seq(1,10)) ),
     ?_assert( lists:seq(1,5) =:= takeN(5, lists:seq(1,10)) ),
     ?_assert( lists:seq(1,10) =:= takeN(20, lists:seq(1,10)) )
    ].

するとomake -Pを立ち上げているターミナルを見るとこんなメッセージが追記で表示されているのがわかるでしょう。ちゃんと保存したらomakeが走ってますね。

*** omake: file src/sample.erl changed
*** omake: rebuilding
- build src <sample>                                                                                                       
+ /opt/erlang/R14B/bin/erlc sample.erl
./sample.erl:58: syntax error before: '.'
./sample.erl:93: unbalanced '-endif'=========================================================              ] 00021 / 00024
./sample.erl:86: function takeN/2 undefined
./sample.erl:87: function takeN/2 undefined
./sample.erl:88: function takeN/2 undefined
./sample.erl:89: function takeN/2 undefined

こんな感じでずーっと編集していくと、ターミナルを眺めながらコードを書いてればよい状態になって幸せです。(Emacsとかならshell-modeで立ち上げていると完全にEmacsの中だけで完結します)
テストだけ先に書いてるのであとは保存したときに下記のように「テスト通りました!」って状況になるまでコード書いてればいいだけ!幸せ!

...
      ./sample.erl:92: unbalanced '-endif'
      ./sample.erl:51: function takeN/3 undefined
*** omake: polling for filesystem changes
*** omake: file src/sample.erl changed
*** omake: rebuilding
- build src <sample>                                                                                                       
+ erl -noshell -s sample test -s init stop
  All 16 tests passed.
*** omake: done (10 min 26.08 sec, 0/0 scans, 6/6 rules, 10/158 digests)                                                   
*** omake: polling for filesystem changes

追記

eunit.hrlへのパス

上記ドキュメントではEUnitを使う際は

-include_lib("eunit/include/eunit.hrl").

を追加しなさいよ。そしてビルドするときは次のようにeunit.hrlを含むeunitという名前のディレクトリパスを追加しなさいよ、と書いてあります。

erlc -pa "path/to/eunit/ebin" $(ERL_COMPILE_FLAGS) -o$(EBIN) $<

しかしいざOTP R14Bの下のディレクトリを検索してみても似たような名前のものはあっても、eunitという名前のディレクトリはありません。

$ find /opt/erlang/R14B -name eunit.hrl
/opt/erlang/R14B/lib/erlang/lib/eunit-2.1.5/include/eunit.hrl

シンボリックリンクでも張ってんのかな?とid:kuenishiにSkypeチャットで聞いてみたところ下記回答。

[10/10/23 9:57:03] kuenishi: include_libは、ertsが適当にパスを読み替えてくれる

分かんねえよ...