Midje ~ Clojureのテストフレームワーク(Part1)

前置き

Clojureには、clojure.testというテスティングフレームワークが標準で含まれています。LeiningenやEclipseで新規プロジェクトを作ると、自動的にテストのスケルトンが生成されます。

しかし、より高機能なフレームワークも存在します。調べてみると、clojure.test.generative, Speclj, ClojureCheck, Midjeなどが見つかりました。その1つ、Midjeについて整理します。情報源は、公式サイトのドキュメントです。

長いので、Part1とPart2の2部構成になってます。

Midjeの概要

  • 実行値と期待値の、ゆるい比較が可能
  • QuickCheckのような、テストデータの自動生成機能は無い(開発中みたい)
  • ボトムアップ開発だけでなく、トップダウン開発でも使えるような工夫がある
  • ソースファイルの更新を監視して自動テストできる
  • 最新の安定バージョンは1.5.1(2013-10-2現在)
  • https://github.com/marick/Midje
  • 名前の由来は、middleと、Clojureのj
  • middleは、プログラマとユーザの間、具体性と抽象性の間、みたいなイメージ
  • Midge(任天堂DSのゲーム「どうぶつの森」のキャラクタ)と掛けたのかも

セットアップ

Leiningenはセットアップ済みと想定します(詳しくは「Clojure開発環境」を参照)。また、本記事ではWindowsを使用します。それ以外の環境については自力で解決して下さい。

まずMidje用のLeiningenプラグインを登録するため、Leiningenのコンフィグファイルを編集しましょう。コンフィグファイルは、%LEIN_HOME%\profiles.cljです。環境変数LEIN_HOMEを定義してない場合は、%USERPROFILE%\.lein\profiles.cljになります。

profiles.clj
{:user {:plugins [[lein-midje "3.1.1"]]}}

2013-10-03時点の最新バージョンは3.1.2ですが、バグってるみたいなので3.1.1を使います。

新規にプロジェクトを作る場合は、midje用のテンプレートを使うことができます。

プロジェクト作成
D:\tmp_work>lein new midje foo

ただ、このテンプレートが生成するproject.cljは、Clojure 1.4.0を使う設定になっています。お好みで修正して下さい。

既存のプロジェクトをMidjeでテストする場合は、project.cljにMidjeを登録します。

project.clj
:profiles {:dev {:dependencies [[midje "1.5.1"]]}}

基本の「き」

以下、新規作成したプロジェクトfooで作業します。

D:\tmp_work>lein new midje foo
Retrieving lein-midje/lein-midje/3.1.1/lein-midje-3.1.1.pom from clojars
Retrieving lein-midje/lein-midje/3.1.1/lein-midje-3.1.1.jar from clojars
Generating a project called 'foo' based on the 'midje' template.

project.cljを、少し修正します。

project.clj
D:\tmp_work>type foo\project.clj
(defproject foo "0.0.1-SNAPSHOT"
  :description "Cool new project to do things and stuff"
  :dependencies [[org.clojure/clojure "1.5.1"]]          ; Clojureのバージョンを修正。
  :profiles {:dev {:dependencies [[midje "1.5.1"]]}})

テストのスケルトンも生成されますが、使うのは先頭の3行だけです。

core_test.clj
D:\tmp_work>type foo\test\foo\core_test.clj
(ns foo.core-test
  (:use midje.sweet)
  (:use [foo.core]))

  ...以下は使わないので削除する...

このように、Midjeを使うにはmidje.sweetをuseします。

実験台として、以下の関数をcore.cljに定義しました。

core.clj
(ns foo.core)

(defn fib [n]                ; フィボナッチ数。
  (cond
    (< n 0) (throw (IllegalArgumentException. "n should be a natural number."))
    (= n 0) 0
    (= n 1) 1
    :else (+ (fib (dec n)) (fib (- n 2)))))

(defn- my-even? [n]          ; 偶数?
  (if (= (mod n 2) 0)
    true
    false))

(defn for-even [lis f]       ; リストlisの偶数値に、関数fを適用した結果。
  (map f (filter my-even? lis)))

fib関数のテストを書いてみましょう。

core_test.clj
(ns foo.core-test
  (:use midje.sweet)
  (:use [foo.core]))

(facts "about fib."
  (fact "if n is 0 or 1, (fib n) should be n."
    (fib 0) => 0
    (fib 1) => 1)
  (fact "if n is larger than 1, like 2, 3, or 4."
    (fib 2) => 1
    (fib 3) => 2
    (fib 4) => 3)
  (tabular "for 5, 6, 7, and 8."
    (fact (fib ?n) => ?e)
    ?n ?e
     5  5
     6  8
     7 13
     8 21)
  (fact "if n is under zero."
    (fib -1) => throws IllegalArgumentException))

factsやfactは、個々のテストケースをグループ化するものです。ネストして構いません。個々のテストケースは、(fib 0) => 0 のように、テストしたい式と、=>と、期待値で構成します。これをMidjeでは、checkableと呼びます。上記のテストには10個のcheckableが記述されています。

実行してみましょう。

D:\tmp_work\foo>lein midje
All checks (10) succeeded.

tabularを使うと、checkableのひな形に対して、複数のテストデータ(と期待値)を供給することができます。上記の例では、(fact (fib ?n) => ?e) がcheckableのひな形で、?nと?eの部分に、5と5、6と8、7と13、8と21を順に供給しています。

また、例外の発生を期待するようなcheckableを記述することもできます(上記テストの最後のcheckable参照)。

続いて、my-even?とfor-evenのテストも書いてみます。my-even?はprivate関数ですが、midje.util/testable-privatesマクロを使うと外へ露出(expose)することができます。

core_test.clj(続き)
(ns foo.core-test
  (:use midje.sweet)
  (:use [midje.util :only (testable-privates)])  ; 追加。
  (:use [foo.core]))

(testable-privates foo.core my-even?)            ; 追加。

...中略...

(facts "about my-even?."
  (fact "if n is even, (my-even? n) should be true."
    (dorun
      (for [n (range 0 100 2)]
        (my-even? n) => true)))
  (fact "if n is odd, (my-even? n) should be false."
    (dorun
      (for [n (range 1 100 2)]
        (my-even? n) => false))))

(facts "about for-even."
  (fact "for-even should filter even numbers."
    (for-even '() identity) => '()
    (for-even '(1 2 3 4) identity) => '(2 4)
    (for-even '(2 4 6) identity) => '(2 4 6)
    (for-even '(1 3 101) identity) => '())
  (fact "for-even should map f to lis."
    (for-even '(1 2 3 4) inc) => '(3 5)
    (for-even '(1 2 3 4) dec) => '(1 3)))

checkableは、factの直下に書く必要はありません。任意のネストレベルに書くことができます。my-even?のテストでは、forマクロにより繰り返しmy-even?を呼んでいます。forは各checkableの結果を遅延シーケンスを返すので、忘れずにdorunする必要があります。dorunしないと、結局、checkableは実行されずに終わります。

private関数をexposeする方法がもう1つあります。midje.util/expose-testablesマクロを使うと、メタデータ^{:testable true}を持つ関数をまとめてexposeできます。

;;; core.clj
(ns foo.core)
(defn- ^:testable private-func1 [])
(defn- ^:testable private-func2 [])
(defn- ^:testable private-func3 [])

;;; core_test.clj
(ns foo.core-test
  (:use midje.sweet)
  (:use [midje.util :only (expose-testables)])
  (:use [foo.core]))

(expose-testables foo.core)

自動テスト

ソースファイルが修正されるたびに自動的にテストを実施することができます。

自動テスト
D:\tmp_work\foo>lein midje :autotest

               ; ↓最初に1回テスト。
======================================================================
Loading (foo.core foo.core-test)
All checks (10) succeeded.

               ; ↓core.cljやcore_test.cljを更新するたびに、自動で再テスト。
======================================================================
Loading (foo.core foo.core-test)
All checks (10) succeeded.

REPLの中から自動テストを開始することもできます。この場合、ソースの監視はバックグラウンドで行われるので、REPLはブロックされません。

自動テストしながらREPL
D:\tmp_work\foo>lein repl
nREPL server started on port 4229 on host 127.0.0.1
REPL-y 0.2.1
Clojure 1.5.1
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)

user=> (use 'midje.repl)
Run `(doc midje)` for Midje usage.
Run `(doc midje-repl)` for descriptions of Midje repl functions.
nil
user=>
user=> (autotest)

======================================================================
Loading (foo.core foo.core-test)
All checks (10) succeeded.
true
user=>

autotest関数の引数により、自動テストを制御することができます。

  • (autotest :pause) ... 一時停止
  • (autotest :resume) ... 再開
  • (autotest :stop) ... 完全停止

checkableの判定

Extended Equality

通常、Midjeは、checkableの => の両辺を =関数で比較します。しかし、これにはいくつかの例外があります。

  • 右辺が正規表現なら、re-find関数で、マッチするかどうかをチェック
  • 右辺が関数なら、その関数を左辺の結果へ適用し、関数が返す値の真偽をチェック
  • 右辺がマップの場合、左辺がrecordでも、内容が同じならOK
extended equalityの例
(defrecord R [x y])
(fact
  "hello" => #"h[el]+o"           ; 正規表現にマッチすればOK。
  1 => odd?                       ; (odd? 1)が真ならOK。
  (R. 1 2) => {:x 1 :y 2})        ; マップじゃなくても、内容が同じならOK。

このような、ゆるい比較の仕組みをextended equalityと呼びます。また、右辺に関数を指定する場合、その関数のことをcheckerと呼びます。

extended equalityは必要に応じて自動的に発動しますが、checkerを自作する際など、明示的に使いたいこともあるでしょう。そのため、midje.checking.core/extended-=という名前で提供されています。

extended-=
(use 'midje.checking.core)
(defn caps? [s] (extended-= s #"^[A-Z]+$"))
(fact
  "ABC" => caps?             ; OK。
  "Clojure" => caps?)        ; NG。

組み込みchecker

checkerは自作することもできますが、Midjeが提供する組み込みcheckerもあります。

  • truthy ... 左辺が真ならOK
  • falsey ... 左辺が偽ならOK
  • anything ... 何でもOK
  • irrelevant ... anythingと同じ

checker生成関数

以下に挙げるのは、Midjeが提供する、checkerを生成するための関数やマクロです(それ自体がcheckerになるわけでは無い)。

contains

containsは、コレクションが部分一致していればOKとするようなcheckerを生成します。要素の比較にはextended equalityを使います。

(fact
  [1 3 5 8] => (contains [5 8])         ; 「5、8」という並びが含まれればOK。
  [1 3 5 8] => (contains 5 8)           ; 括弧は省略できる。
  [1 3 5 8] => (contains [odd? even?])  ; 要素に関数を指定。「奇数、偶数」の並びがあればOK。
  [1 3 5 8] => (contains odd? even?))   ; やはり括弧は省略できる。
(fact
  '("a" 1 8) => (contains #"[a-z]"))    ; 小文字を含む文字列があればOK。

(defrecord R [x y])
(fact (R. 1 2) => (contains {:x odd?})) ; 左辺がマップ的なもので、キー:xの値が奇数ならOK。

extended equalityが自動的に発動するのは、コレクションのトップレベルの要素のみです。ネストしたコレクションに再帰的に適用されるわけではありません。

(fact
  [1 3 [5] 8] => (contains 5)              ; NG。
  [1 3 [5] 8] => (contains (contains 5)))  ; OK。

containsにはオプションを指定することができます。

  • :gaps-ok ... 並びの途中に余計なものが紛れてもOK
  • :in-any-order ... 順不同とする
(fact
  [1 2 3 4 5] => (contains [1 2 4])                       ; NG。
  [1 2 3 4 5] => (contains [1 2 4] :gaps-ok)              ; OK。
  [5 1 4 2] => (contains [1 4 5] :in-any-order)           ; OK。
  [5 1 4 2] => (contains [1 2 5] :gaps-ok)                ; NG。
  [5 1 4 2] => (contains [1 2 5] :gaps-ok :in-any-order)) ; OK。

just

justは、コレクションが要素の過不足なく一致していればOKとするようなcheckerを生成します。containsと同様、要素の比較にはextended equalityを使います。

(fact {:a 1, :b 2} => (just {:a 1, :CCC 333}))     ; NG(過不足あり)。
(fact {:a 1, :b 2} => (just {:a odd? :b even?}))   ; OK。
(fact [1 2 3] => (just [odd? even? odd?]))         ; OK。
(fact [1 2 3] => (just odd? even? odd?))           ; 括弧は省略可。
(fact [[[1]]] => (just (just (just odd?))))        ; OK。ネストしたら自力で再帰。
(fact [2 1 3] => (just [1 2 3] :in-any-order))     ; OK(順不同)。
(fact [1 3] => (just [odd? 1] :in-any-order))      ; OK(順不同)。
(fact #{3 8 1} => (just odd? 3 even?))             ; OK(セットに対しては、自動的に順不同)。

has-prefixとhas-suffix

has-prefixとhas-suffixは、コレクションの先頭部分か最後尾部分が部分一致していればOKとするようなcheckerを生成します。

(fact
  [1 2 3] => (has-prefix [1 2])
  [1 2 3] => (has-prefix [2 1] :in-any-order)
  [1 2 3] => (has-suffix [3]))

n-of

n-ofは、コレクションの要素数と要素の性質をチェックするようなcheckerを生成します。

要素数が1から10までなら、one-of、two-ofといった具合に専用の関数を使い、10を超えるとn-ofを使います。

(fact
  ["a"] => (one-of "a")                      ; 1個の"a"。
  [:k :w] => (two-of keyword?)               ; 2個のキーワード。
  ["a" "aa" "aaa"] => (three-of #"a+")       ; 3個の、#"a+"にマッチする文字列。
  #{1 3 5 7} => (four-of odd?)               ; 4個の奇数。
  (repeat 100 "a") => (n-of "a" 100))        ; 100個の"a"。

has

hasは、Clojureの量化関数(quantification functions)を使ってコレクションをチェックするようなcheckerを生成します。量化関数とは、every?、not-every?、some、not-any?などを指します。

hasは、以下のようなフォームで使われます。collはコレクション、fは量化関数、predは期待値です。

(fact coll => (has f pred))

これをテスト実行すると、(f pred coll)が呼ばれ、その真偽がテスト結果になります。さらに、predの部分に正規表現を指定すると、extended equalityが発動します。

(fact
  [2 4 8] => (has not-any? odd?)                   ; OK。奇数を1つも含まないか?
  {:a 1, :b 3} => (has every? odd?)                ; OK。マップの各値が全て奇数か?
  #{1 2 3} => (has not-every? odd?)                ; OK。奇数でないものが1つ以上あるか?
  ["ab" "hi, mom!" "aaab"] => (has every? #"a+b")  ; NG。全てが#"a+b"にマッチするか?
  ["ab" "hi, mom!" "aaab"] => (has some #"a+b"))   ; OK。#"a+b"にマッチする要素があるか?

exactly

=>の左辺が関数の場合、右辺にも関数を指定しますが、そうすると右辺の関数はcheckableと解釈されてしまいます。右辺の関数を文字通り関数として解釈させたい場合は、exactlyを使います。

(defn inc_or_dec [b] (if b inc dec))
(fact
  (inc_or_dec true) => inc              ; これではNG。
  (inc_or_dec false) => dec             ; これではNG。
  (inc_or_dec true) => (exactly inc)    ; これならOK。
  (inc_or_dec false) => (exactly dec))  ; これならOK。

つまりexactlyは、引数に指定された関数と、=>の左辺が一致するかどうかをチェックするようなcheckerを生成します。

roughly

roughlyは、左辺が、ある範囲内に収まっているかどうかをチェックするようなcheckerを生成します。 (roughly expected delta)のように、中心値expectedと誤差deltaで許容範囲を指定します。左辺がexpected±deltaならOKです。

(fact
  8 =not=> (roughly 10 1)
  9 => (roughly 10 1)
  10 => (roughly 10 1)
  11 => (roughly 10 1)
  12 =not=> (roughly 10 1))

deltaは省略可能で、デフォルト値は1000分の1です。

throws

throwsは、左辺で起きた例外の内容をチェックするようなcheckerを生成します。

throwsの引数には、クラスやメッセージ文字列、正規表現、及び述語を指定することができます。

(fact
  (/ 10 0) => (throws ArithmeticException)
  (/ 10 0) => (throws "Divide by zero")
  (/ 10 0) => (throws ArithmeticException "Divide by zero")
  (/ 10 0) => (throws #"by")
  (/ 10 0) => (throws ArithmeticException #"by" #"zero")
  (/ 10 0) => (throws #"Divide" #"by" #"zero"))
(fact
  (nth [] 0) => (throws IndexOutOfBoundsException)
  (nth [] 0) => (throws #(nil? (.getMessage %))))   ; OK。getMessage()がnullを返す。

throwsの引数に述語を指定した場合、その述語の引数には左辺で発生した例外オブジェクトが指定されます。

every-checkerとsome-checker

every-checkerとsome-checkerは、複数のcheckerを組み合わせたcheckerを生成します。

every-checkerで生成したcheckerは、組み合わせた全てのcheckerがOKならOKになります。some-checkerで生成したcheckerは、組み合わせたcheckerのどれか1つでもOKならOKになります。

(fact
  4 => (every-checker odd? (roughly 3))       ; NG(odd?でなく、3±0.003でもない)。
  4 => (every-checker even? (roughly 3))      ; NG(3±0.003でない)。
  4 => (every-checker even? (roughly 3 1))    ; OK(全部OK)。
  4 => (some-checker odd? even? (roughly 3))) ; OK(どれかがOK)。

(defrecord R [x y])
(fact (R. 1 2) => (every-checker #(instance? R %) (contains {:x 1})))  ; OK。

every-checkerの代わりに、andで無名関数を作っても同じことができそうですが、NGのときのメッセージが微妙に異なります。every-checkerでNGになった場合、Midjeは、どのcheckerがNGだったのかを教えてくれますが、andで作った無名関数の場合は、そこまでの情報はレポートされません。

user=> (fact 10 => (every-checker even? odd? (roughly 3)))

FAIL at (D:\tmp\realtmp\form-init6830933947093971652.clj:1)
Actual result did not agree with the checking function.
        Actual result: 10
    Checking function: (every-checker even? odd? (roughly 3))
    During checking, these intermediate values were seen:
       odd? => false                   ; ←odd?でNGになったことが分かる。
false
user=> (fact 10 => #(and (even? %) (odd? %) ((roughly 3) %)))

FAIL at (D:\tmp\realtmp\form-init6830933947093971652.clj:2)
Actual result did not agree with the checking function.
        Actual result: 10
    Checking function: (fn* [p1__8416#] (and (even? p1__8416#) (odd? p1__8416#)
((roughly 3) p1__8416#)))              ; ←無名関数内でNGになったことしか分からない。
false
user=>

chatty-checker

chatty-checkerは、おしゃべりなcheckerを生成します。これを使うと、NGになったときに、より詳しい情報を知ることができます。chatty-checkerフォームは、fnフォームと同じ構造を取ります。chatty-checkerを使わない場合と使う場合のNGメッセージを比較してみましょう。左辺に1を足した値が偶数になるかどうかをチェックします。

user=> (fact 4 => (fn [actual] (even? (inc actual))))

FAIL at (D:\tmp\realtmp\form-init6830933947093971652.clj:1)
Actual result did not agree with the checking function.
        Actual result: 4
    Checking function: (fn [actual] (even? (inc actual)))   ; ←無名関数の結果がNGだった。
false
user=> (fact 4 => (chatty-checker [actual] (even? (inc actual))))

FAIL at (D:\tmp\realtmp\form-init6830933947093971652.clj:1)
Actual result did not agree with the checking function.
        Actual result: 4
    Checking function: (chatty-checker [actual] (even? (inc actual)))
    During checking, these intermediate values were seen:
       (inc actual) => 5                                    ; ←incした結果が5だったのでNG。
false
user=>

小技

future fact

factやfactsの代わりにfuture-factやfuture-factsを使うと、「今すぐにはOKにできないけど、いずれは通す必要があるcheckable」を書くことができます。

(defn some-func [])
(future-fact
  (some-func) => truthy)

これらは実行されませんが、以下のように表示されるので、TODOリスト的な目的に使えるでしょう。

D:\tmp_work\foo>lein midje

WORK TO DO at (core_test.clj:24)      ; ←ここ。
All checks (10) succeeded.

=>のバリエーション

checkableで使う矢印には、バリエーションがあります。

  • =not=> ... 左辺が右辺と一致しなければOK
  • =deny=> ... =not=>と同じ
  • =expands-to=> ... マクロ展開の結果をチェック
  • =future=> ... 当該checkableをfuture-factとして扱う
(fact
  (some even? [1 5]) =not=> truthy         ; OK。
  (some even? [1 5]) =deny=> truthy        ; OK。
  (when true (prn "yes")) =expands-to=> (if true (do (prn "yes")))   ; OK。
  true =future=> falsey)                   ; これは実行されない。

困ったことに、=>と =not=>は、補完関係ではありません。以下の2つのcheckableは、どちらもNGになります。

(defrecord R [x y])
(defrecord R2 [x y])
(fact
  (R. 1 2) => (R2. 1 2)
  (R. 1 2) =not=> (R2. 1 2))

つまり、=>も、=not=>も、少なくとも左右が同じ型でないとOKにならないのです。

(fact (R. 1 2) =not=> (R. 2 3))     ; これならOK。

tabular

tabularは、clojure.testのareに相当します。

(are [x y] (= (+ x y) (* x y))
     0 0
     2 2)
(tabular
  (fact (+ ?x ?y) => (* ?x ?y))
  ?x ?y
  0 0
  2 2)

tabularでは、オペレータやcheckableの矢印もパラメータ化することができます。

(tabular
  (fact (+ ?x ?y) ?a (?ope ?x ?y))
  ?x ?y ?a ?ope
  0 0 => *
  2 2 => *
  1 2 =not=> -
  3 3 =not=> *)

Part2へ続きます。

Last modified:2013/10/05 12:38:46
Keyword(s):
References:[Clojureコードペット] [FP: 関数型プログラミング] [Midje ~ Clojureのテストフレームワーク(Part2)]
This page is frozen.