怖くないテスト駆動開発(CommonLisp+FiveAM編) 

# 怖くないテスト駆動開発(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`マクロの構文について軽く説明します。

~~~lisp
(defsystem <パッケージ名>
:author "作者名"
:license "ライセンス"
:description "システムの概要"
:depends-on ("依存システム1" "依存システム2" ... )
~~~

と言った形で記述することが出来ます。
CommonLispに親しんだ方なら、なんとなく分かるかとおもいます。
しかしこれ以外が癖もので、まず`:components`というのがあります。

~~~lisp
:components ((:module "src"
              :compoents ((:file "ファイル名1" :depends-on "ファイル名2")
			              (:file "ファイル名2"))))
						  
~~~

こんな感じで記述します。今記事では余り重要ではないので、
詳しくは説明しませんが、依存関係の記述などが可能です。
`:serial t`など一括した依存関係の記述も可能です。
また、ファイル名は自動で`.lisp`拡張子がつくので不要です。

あと、`:in-order-to`というがあります。
これは、オーペレータの依存関係(？)を記述するものです。

~~~lisp
:in-order-to ((<対象となるオペレータ> (<依存してるオペレータ> <コンポーネント>)))
~~~

と書くことができます。所謂フックとして動作し、対象オペレータを
実行する際に、実行前の処理として指定したオペレータを実行します。
これについてもオペレータと同様にテストフレームワークの説明の際に
詳しく説明します。

以上で非常にざっくりですがASDFの説明を終りにして、FiveAMの説明に移ります。

## FiveAMについて
CommonLispには数多くのテストフレームワークがあります。
そのなかで、よく使用されているのものとしてFiveAMがあります。
最近だとProveの利用も(僕の観測範囲では)多いですね。

とりあえず、一つ使い方が分かればよいのでFiveAMにしてみました。

FiveAMは結構古くから使われているフレームワークらしく、
かの有名な`drakma`というHTTPクライアントでも利用されていますね。

## よく使われるAPI

### `def-suite`
FiveAMにはスイートという概念があり、テスト群を束ねる最大の単位です。
これの名前を定義するマクロです。

~~~lisp
(def-suite <スイート名>)
~~~

### `in-suite`
`in-package`と同じで、定義したスイートに入るためのマクロ

~~~lisp
(in-suite <スイート名>)
~~~

### `test`
テストを定義するためのマクロ、スイートの次に大きい単位です。

~~~lisp
(test
  <テスト式1>
  <テスト式2>
  ...)
~~~

### `is`
テストの最小単位。真であるかどうかでテストの成功を決める。

~~~lisp
(is <S式>)
~~~

大体これくらい知っていれば、例を理解するには十分でしょう。

## 実例

さて役者はそろったので、そろそろ実例をやってみましょう。

例題の要件

- システム名は`foo`
- テストフレームワークはFiveAMを用いる
- 足し算関数`add`をテストする。

### システムをつくる

まず、テスト対象となるシステムをつくりましょう。

~~~lisp
; ファイル名 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`オペレータの実行が必要だと記述してあるわけです。

~~~lisp
; ファイル名 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を参考にしています。

~~~lisp
:ファイル名 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メソッドですが、これは、

~~~lisp
(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`を一括で実行すします。

次にテストコードです。

~~~lisp
; ファイル名 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で実行してみます。

~~~lisp
* (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の雛形を生成しています。

~~~lisp
#|-*- 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`とおなじですね!

## まとめ
なんか、最後の方はソースコードに注釈を加える程度になってしまいましたが、
一応僕がテストフレームワークを使っていて分からなかったところは
全て抑えたつもりです。理解の補助となれれば幸いです。
詳しくは、参考文献に提示したそれぞれのマニュアルをご覧ください。

## 参考文献

- [ASDF Manual](https://common-lisp.net/project/asdf/asdf.html)
- [FiveAM API Reference](https://common-lisp.net/project/fiveam/docs/index.html)
- [Drakma](https://github.com/edicl/drakma)