LoginSignup
17
19

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-07

怖くないテスト駆動開発(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とおなじですね!

まとめ

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

参考文献

17
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
19