FiveAM - lisp-cookbook-ja/common-lisp GitHub Wiki

ライブラリ テスト FiveAM

FiveAMはシンプルなユニットテストフレームワークです。

  • テストスイートによるテストの階層化
  • テスト同士の依存関係を定義できる
  • 対話的なインターフェイスのサポート
  • QuickCheck式のSpecification-based testingへの対応

といった特徴があります。


$$toc


導入方法

Quicklispに登録されています (Quicklispを使う)

(ql:quickload 'fiveam)

利用例

> (5am:test add-2
    "ADD-2関数のテスト"   ; 短かい説明
    ;; チェックする内容
    (5am:is (= 2 (add-2 0)))
    (5am:is (= 0 (add-2 -2))))
ADD-2
> (5am:run! 'add-2)
..
 Did 2 checks.
    Pass: 2 (100%)
    Skip: 0 ( 0%)
    Fail: 0 ( 0%)

NIL
>

テストの定義

5am:testマクロによってテストを定義できます。

(5am:test identity
  "Test for cl:identity."
  (5am:is (zerop (identity 0))))

最初の引数はテストに付ける名前で、それ以降はテストの処理の本体です。また、defunなどと同じで、処理の前にテストの説明を書くことができます。この説明はテストが失敗されたときに表示されます。

> (5am:test with-description "Description" (5am:fail "Failed"))
WITH-DESCRIPTION
> (5am:test without-description (5am:fail "Failed"))
WITHOUT-DESCRIPTION
> (5am:run! '(with-description without-description))
ff
 Did 2 checks.
    Pass: 0 ( 0%)
    Skip: 0 ( 0%)
    Fail: 2 (100%)

 Failure Details:
 --------------------------------
 WITHOUT-DESCRIPTION []: 
      Failed.
 --------------------------------
 --------------------------------
 WITH-DESCRIPTION [Description]: 
      Failed.
 --------------------------------

NIL
>

アサーション

以下のようなアサーションが定義されています。

:5am:is: 渡された式が真になるときに成功する :5am:skip: スキップする :5am:is-true: 渡された式が真になるときに成功する。失敗した場合のメッセージに5am:isと差がある :5am:is-false: 渡された式が偽になるときに成功する :5am:signals: 指定されたコンディションが発生すると成功する :5am:finishes: 渡された式が最後まで実行できた場合に成功する :5am:pass: 常に成功する :5am:fail: 常に失敗する

具体的な使用例です。

;; TODO: もっと例を増やす

;; symbolがキーワードのときに成功する
(5am:is-true (keywordp symbol))
(5am:is (equal "KEYWORD" (package-name (symbol-package symbol))))

;; does-not-existというファイルが存在しない場合に成功する
(5am:is-false (probe-file #p"does-not-exist"))
(5am:signals file-error (close (open #p"does-not-exist")))

コンパイルのタイミング

特に指定しない限り、テストの処理の本体は、テストを実行するときに初めてコンパイルされますが、テストを定義する時点でコンパイルしてしまうこともできます。ローカルな変数や関数を利用するときに便利です。

;; ローカルな関数を利用する例
(flet ((ref (fn)
         (5am:is (eql #\b (funcall fn "abc" 1)))))
  (5am:test (char :compile-at :definition-time)
    (ref #'char))
  (5am:test (elt :compile-at :definition-time)
    (ref #'elt)))

このように、テストの名前を「(名前 :compile-at :definition-time)」という形にすることで、コンパイルのタイミングを指定できます。


テストの階層化

テストスイートを定義することでテストを階層化できます。スイートの仕組みはパッケージに良く似ていて、5am:def-suiteマクロでスイートを定義して、5am:in-suiteマクロで現在のスイートを切り替えるようになっています。

;; すべてのテストが属するallというスイートを定義する
(5am:def-suite all)

;; allに属するsuite-aとsuite-bというスイートを定義する
(5am:def-suite suite-a :in all)
(5am:def-suite suite-b :in all)

;; suite-aに属するaというテストを定義する
(5am:in-suite suite-a)
(5am:test a "Test A" (5am:pass))

;; suite-bに属するbというテストを定義する
(5am:in-suite suite-b)
(5am:test b "Test B" (5am:fail))

5am:def-suiteと5am:in-suiteの組み合わせは良く使われるので、同じ処理をする5am:def-suite*というマクロが定義されています。

;;; 上と同等の例をdef-suite*で

(5am:def-suite all)

(5am:def-suite* suite-a :in all)
(5am:test a "Test A" (5am:pass))

(5am:def-suite* suite-b :in all)
(5am:test b "Test B" (5am:fail))

テストの実行

定義したテストやスイートを実行する場合、基本的に5am:run!という関数を利用します。これは、指定に従ってテストを実行し、テストの結果をわかりやすく表示するという、ユニットテストで幾度となく繰り返される処理を組み合わせたものです。

> (5am:run!)
..
 Did 2 checks.
    Pass: 2 (100%)
    Skip: 0 ( 0%)
    Fail: 0 ( 0%)

NIL
>

標準では、検査が成功するごとにドット(.)が表示され、失敗するごとにfが表示されます。テストコードでエラーが発生した場合はXが表示されます。すべてのテストが終了するか、途中でテストが中断されるまでこれは繰り返され、それが終わるとテスト結果の集計が表示されます。

5am:run!で何を実行するかは引数を渡すことで指定できます。テストやスイートに関連付けられたシンボル、テストやスイートのオブジェクト、それらのシンボルやオブジェクトを要素とするリストがサポートされています。

;; xというシンボルに関連付けられたテストあるいはスイートを実行する
(5am:run! 'x)

;; xとyというシンボルに関連付けられたテストあるいはスイートを実行する
(5am:run! '(x y))

引数が省略された場合、現在のスイートに属するテストが実行されます。

対話的なインターフェイス

デバッガとの連携

テストを実行するとき、標準ではテストが失敗したりテストコードでエラーが発生した場合でも、デバッガに入ることはありません。この動作を変えたい場合、5am:debug-on-failure5am:debug-on-errorというスペシャル変数の値を変更します。

> (5am:run!)
f
 Did 1 check.
    Pass: 0 ( 0%)
    Skip: 0 ( 0%)
    Fail: 1 (100%)

 Failure Details:
 --------------------------------
 B [Test B]: 
      no reason given.
 --------------------------------

NIL
> (let ((5am:*debug-on-failure* t)) (5am:run!))
The following check failed: NIL
no reason given.
   [Condition of type IT.BESE.FIVEAM::CHECK-FAILURE]

Restarts:
 0: [IGNORE-FAILURE] Continue the test run.
 1: [RETEST] Rerun the test #<TEST-CASE B #x30200132055D>
 2: [IGNORE] Signal an exceptional test failure and abort the test #<TEST-CASE B #x30200132055D>.
 3: [EXPLAIN] Ignore the rest of the tests and explain current results
 4: [RETRY] Retry SLIME REPL evaluation request.
 5: [*ABORT] Return to SLIME's top level.
...

なお、この機能を簡単に利用できるように、5am:debug-on-errorと5am:debug-on-failureをtにした状態で5am:run!を呼ぶ5am:debug!という関数が定義されています。

再実行

直前三回分のテストの実行を、5am:!5am:!!5am:!!!という関数で繰り返すことができます。5am:!が一番新しく、5am:!!!が一番古いものです。


Specification-based testing

テストするデータを自動的に生成できます。ユーザ定義のジェネレータを使うこともできます。

> (defun plus (a b) (+ a b))
PLUS
> (5am:test plus
    (5am:for-all ((a (5am:gen-integer))
                  (b (5am:gen-integer)))
      (5am:is (= (+ a b) (plus a b)))
      (5am:is (= (plus a b) (plus b a)))
      (5am:is (= a (plus a 0)))
      (5am:is (= 0 (plus a (- a))))
      (5am:is (< a (plus a 1)))
      (5am:is (= (* 2 a) (plus a a)))))
PLUS
> (5am:run! 'plus)
...............................................................................
...............................................................................
...............................................................................
...............................................................................
...............................................................................
...............................................................................
...............................................................................
................................................
 Did 1 check.
    Pass: 1 (100%)
    Skip: 0 ( 0%)
    Fail: 0 ( 0%)

NIL

参考資料

参考リンク