怖くないテスト駆動開発(CommonLisp+FiveAM編)
ASDFについて
テストフレームワークとは直接関係ありませんが、
テスト駆動開発する上でASDFについて少しでも知っておくのは
後々便利なので必要なところだけ解説します。
Another System Definition Facilityの略であり、
ライブラリ(ASDFでは「システム」といいます)の読み込みを
サポートするツール群です。
CommonLispでは所謂デファクトスタンダードとなっており、
事実、多くの処理系(SBCLやCLISPなど)では標準で組み込まれています。
Lispにおいてもファイルの読み込み順序は当然重要であり
ファイルの依存関係を解決するのために使われています。
ASDFは<システム名>.asd
というファイルをもとに
システムの読み込みを行います。
このファイルには、ファイルの依存関係の他に、
依存している外部のシステム、作者、バージョン、
概要、システムのホームページ(!?)まで
様々なことを記述することができます。
このデータはGemやPIPに相当するQuicklisp等で利用されています。
defsystem
について
ASDFは様々な機能を提供していますが、その中核を成す
defsystem
マクロについて説明します。
先程ASDFは<システム名>.asd
ファイルを使うと書きましたが、
このファイルに記述されてるものが、
まさにdefsystem
マクロをつかったシステムの定義です。
オペレータについて
ASDFではオペレータという概念があります。
これは、ASDFがシステムに対して行う操作であり、
CLOSの総称関数を利用して実装されています。
perform
メソッドを再定義することで、
ASDFの処理をハックすることができます。
定義済みのオペレータとして
- test-op
- load-op
- compile-op
- prepare-op
- load-source-op
- compile-bundle-op
などがあります。
とくにtest-op
については、テストフレームワークの解説をするときに
細かく説明します。
構文
defsystem
マクロの構文について軽く説明します。
(defsystem <パッケージ名>
:author "作者名"
:license "ライセンス"
:description "システムの概要"
:depends-on ("依存システム1" "依存システム2" ... )
と言った形で記述することが出来ます。
CommonLispに親しんだ方なら、なんとなく分かるかとおもいます。
しかしこれ以外が癖もので、まず:components
というのがあります。
:components ((:module "src"
:compoents ((:file "ファイル名1" :depends-on "ファイル名2")
(:file "ファイル名2"))))
こんな感じで記述します。今記事では余り重要ではないので、
詳しくは説明しませんが、依存関係の記述などが可能です。
:serial t
など一括した依存関係の記述も可能です。
また、ファイル名は自動で.lisp
拡張子がつくので不要です。
あと、:in-order-to
というがあります。
これは、オーペレータの依存関係(?)を記述するものです。
:in-order-to ((<対象となるオペレータ> (<依存してるオペレータ> <コンポーネント>)))
と書くことができます。所謂フックとして動作し、対象オペレータを
実行する際に、実行前の処理として指定したオペレータを実行します。
これについてもオペレータと同様にテストフレームワークの説明の際に
詳しく説明します。
以上で非常にざっくりですがASDFの説明を終りにして、FiveAMの説明に移ります。
FiveAMについて
CommonLispには数多くのテストフレームワークがあります。
そのなかで、よく使用されているのものとしてFiveAMがあります。
最近だとProveの利用も(僕の観測範囲では)多いですね。
とりあえず、一つ使い方が分かればよいのでFiveAMにしてみました。
FiveAMは結構古くから使われているフレームワークらしく、
かの有名なdrakma
というHTTPクライアントでも利用されていますね。
よく使われるAPI
def-suite
FiveAMにはスイートという概念があり、テスト群を束ねる最大の単位です。
これの名前を定義するマクロです。
(def-suite <スイート名>)
in-suite
in-package
と同じで、定義したスイートに入るためのマクロ
(in-suite <スイート名>)
test
テストを定義するためのマクロ、スイートの次に大きい単位です。
(test
<テスト式1>
<テスト式2>
...)
is
テストの最小単位。真であるかどうかでテストの成功を決める。
(is <S式>)
大体これくらい知っていれば、例を理解するには十分でしょう。
実例
さて役者はそろったので、そろそろ実例をやってみましょう。
例題の要件
- システム名は
foo
- テストフレームワークはFiveAMを用いる
- 足し算関数
add
をテストする。
システムをつくる
まず、テスト対象となるシステムをつくりましょう。
; ファイル名 foo.asd
(in-package :cl-user)
(defpackage foo-asd
(:use :cl :asdf))
(in-package :foo-asd)
(defsystem foo
:version "0.0.0"
:author "ta2gch"
:license "UNLICENSE"
:components ((:module "src" :components ((:file "foo"))))
:in-order-to ((test-op (test-op foo-test))))
ここで:in-order-to
が出てくるわけですが、
つまりtest-op
オペレータを実行するためには、
foo-test
システムのtest-op
オペレータの実行が必要だと記述してあるわけです。
; ファイル名 src/foo.lisp
(in-package :cl-user)
(defpackage foo
(:use :cl)
(:export :add))
(in-package :foo)
(defun add (a b) (+ a b))
肝心のfooパッケージには、add関数のみが定義されており、add関数はexportされています。
テストコードを書く
では、テスト用のfoo-test
システムをつくりましょう。
ここではDrakmaを参考にしています。
:ファイル名 foo-test.asd
(in-package :cl-user)
(defpackage foo-test-asd
(:use :cl :asdf :uiop))
(in-package :foo-test-asd)
(defsystem :foo-test
:version "0.0.0"
:author "TANIGUCHI Masaya"
:license "UNLICENSE"
:depends-on (:foo :fiveam)
:components ((:module "t" :components ((:file "foo"))))
:perform (test-op (o s)
(symbol-call :fiveam :run! :foo)))
テスト用のシステムは当然、対象となるfoo
システムと、
テストフレームワークであるFiveAMに依存しているので:depends-on
で
あらかじめ依存関係を記述しておきます。
また、先ほど説明にあったperformメソッドですが、これは、
(defmethod perform (o test-op) (s (eql (find-components "foo-test")))
(symbol-call :fiveam :run! :foo))
を定義したときと同様の効果があります。
ちなみに、symbol-call
はUIOPに定義されている関数で、
:fiveam
パッケージのrun!
に:foo
を引数として渡して実行しています。
ここでいうfoo
とは、テストコードで定義されたスイート名です。
run!
関数は、テストの実行する関数run
とテスト結果を表示する
関数explain
を一括で実行すします。
次にテストコードです。
; ファイル名 t/foo.lisp
(in-package :cl-user)
(defpackage foo-test
(:use :cl :fiveam))
(in-package :foo-test)
(def-suite :foo)
(in-suite :foo)
(test foo-test
(is (= 1 (foo:add 0 1)))
(is (= 1 (foo:add 2 -1))))
まぁ、説明したとおりですね。
解説の必要もないでしょう。
テストを実行してみる。
とりあえず、SBCLで実行してみます。
* (ql:quickload :fiveam)
* (load #p"foo.asd")
* (load #p"foo-test.asd")
* (asdf:test-system :foo)
Running test suite EXAMPLE
Running test EXAMPLE-TEST ..
Did 2 checks.
Pass: 2 (100%)
Skip: 0 ( 0%)
Fail: 0 ( 0%)
asdf:test-system
で対象システムのtest-opを叩き、
:in-order-to
で指示しておいたテスト用のシステムのtest-opが実行される
ということになっています。
こうすることで、テスト以外の読み込みではFiveAMは依存関係にはいらないので
無駄な読み込みを避けることができます。
Lakefileをつかって楽をする。
roswellユーザーはLakeというコマンドラインユーティリディをつかうと
幸せになれるかもしれません。rubyで言う、rakeとかに相当するタスクランナーです。
$ ros install lake
$ lake-tools init
Lakefileの雛形を生成しています。
#|-*- mode:lisp -*-|#
(in-package :cl-user)
(defpackage :lake.user
(:use :cl :lake :cl-syntax :asdf)
(:shadowing-import-from :lake
:directory))
(in-package :lake.user)
(use-syntax :interpol)
(task "test" ()
(load #p"foo.asd")
(load #p"foo-test.asd")
(asdf:test-system :foo))
;;; here follow your tasks...
testというタスクを加えました。
$ lake test
まるで、make test
とおなじですね!
まとめ
なんか、最後の方はソースコードに注釈を加える程度になってしまいましたが、
一応僕がテストフレームワークを使っていて分からなかったところは
全て抑えたつもりです。理解の補助となれれば幸いです。
詳しくは、参考文献に提示したそれぞれのマニュアルをご覧ください。