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

前置き

Clojureのテスティングフレームワークの1つ、Midjeに関するまとめのPart2です。Part1はこちらです。

この記事の情報は公式サイトのドキュメントに基づいていますが、実際には試してないので、英文の読解ミスなどを含んでいる可能性があります。

特定のcheckableだけをテストする

名前空間やフォルダパスでフィルタリング

leinで
lein midje namespace1 namespace2    ;☆ディレクトリ名でもいい?
lein midje namespace.*
REPLで
(autotest :dir "test/foo")
(autotest :dirs "test/foo" "src/foo")

メタデータでフィルタリング

  • factに付けたメタデータでフィルタリング
  • メタデータの名前は任意
(fact "a test that takes long" :slow ...)  ; メタデータ^:slowをtrueにセット。
(fact "a crucial test" :core ...)          ; メタデータ^:coreをtrueにセット。
(fact {:priority 1} ...)                   ; ^{:priority 1}
(fact {:priority 5} ...)                   ; ^{:priority 5}
leinで
lein midje :filter core            ; :coreのみテスト。
lein midje :filter -slow           ; :slow以外をテスト。
lein midje :filter core -slow      ; :coreか、:slow以外をテスト。
REPLで
(autotest :filter :core)                    ; :coreのみ。
(autotest :filter :core :dir "test/m")      ; ☆AND? OR?
                                            ; 複数なら、:filtersや:dirsにする。
(autotest :filter (complement :slow))       ; :slow以外。
(autotest :filter #(>= (:priority %) 3))    ; :priorityの値が3以上。

SetupとTeardown

  • テストの準備と後始末処理
  • ☆発展途上の機能っぽい
  • with-state-changesと、before、after、aroundで指定する
before(準備)
(def state (atom nil))

(with-state-changes
  [(before :facts (reset! state 0))]   ; :factsのあとに、準備処理用の式を1つ書ける。
  (fact ...)
  (fact ...)
  ...)
  • with-state-changes&factをネストさせると、内側のfact実施前に、外側のsetupが再実施される
  • イマイチなので変える予定
after(後始末)
(with-state-changes
  [(after :facts (swap! state dec))]
  (fact ...)
  (fact ...))
ネスト
(with-state-changes [(before :facts (println "outer in"))
                     (after :facts (println "outer out"))]
  (with-state-changes [(before :facts (println "  inner in"))
                       (after :facts (println "  inner out"))]
    (fact (+ 1 1) => 2)))          ; outer in, inner in, inner out, outer outの順。
around(前後)
(with-state-changes
  [(around :facts (transaction ?form (rollback)))]
  (fact ...)             ; ?formの部分に、各factのボディ部が入る。
  (fact ...))
  • with-state-changesの代わりにnamespace-state-changesを使うと、そのbefore、after、aroundが、以降の全てのfactに適用される
(namespace-state-changes [(before :facts (reset! state 0))])
(fact ...)                  ; setup付き。
(fact ...)                  ; setup付き。
(fact ...)                  ; setup付き。
(namespace-state-changes)   ; クリア。
(fact ...)                  ; setupなし。
(fact ...)                  ; setupなし。

トップダウン開発を意識した機能

ビデオ

Midjeを発明したBrian Marick氏のビデオが参考になります。英語なので、何となくしか分かりませんが。

Prerequisites

トップダウンで開発する場合、関数Aから呼ぶ予定のサブ関数Bを実装する前に、関数Aをテストしたい、というニーズが生じます。通常ならサブ関数Bのスタブを書くところですが、Midjeでは、関数Aのfactの中に「前提条件」として、「関数Aは、関数Bをこの引数で呼ぶはずで、関数Bはこれを返すはず」といった情報を記述することができます。

(unfinished read-project-file)       ; 未完成のサブ関数を宣言。

(defn fetch-project-paths []         ; テスト対象関数。
  ...
  ...)

(fact
  (fetch-project-paths) => ["test1" "test2" "source1"]
  (provided                          ; サブ関数呼び出しに関する前提条件(prerequisite)。
    (read-project-file) => {:test-paths ["test1" "test2"]
                            :source-paths ["source1"]})
                                     ; read-project-fileを引数なしで呼ぶ。
                                     ; マップを返す。

  (fetch-project-paths) => ["test"]
  (provided
    (read-project-file "") =throws=> (Error. "boom!")))
                                     ; read-project-fileを引数""で呼ぶ。
                                     ; =throws=>により、サブ関数が例外を投げることを表明。
  • providedには複数のprerequisiteを指定できる
  • もし、指定したprerequisiteが1度も呼ばれなかったら、そのテストはNG

Metaconstants

上記例の"test1"や"test2"のような、「何でも良い適当な値」として、metaconstantを指定できます。

(fact
  (fetch-project-paths) => [..test1.. ..test2.. ..source..]
  (provided
    (read-project-file) => {:test-paths [..test1.. ..test2..]
                            :source-paths [..source..]}))
  • metaconstantの名前は、ドットかハイフンで囲む(.mc.とか--mc--とか)
  • ドットやハイフンの数が違っても、同じものとして扱う(.mc.と..mc..は同じ)
  • Clojureのドットには特殊な意味があるので、誤解されそうな場所ではハイフンを使うしかない
  • typoを許容する(..mc...のように、ドットが欠けても許す)

AOTコンパイルすると、metaconstantの箇所でエラーが出る場合があるそうです。その場合、以下のようにmetaconstant名を宣言してやると、解消できるかもしれません。

(metaconstants ..m.. .mc.)

prerequisiteの引数マッチング

テスト時にサブ関数が呼ばれると、Midjeは、providedで指定された複数のprerequisiteの中から、関数名と引数がマッチするものを探します。このときの引数マッチングには、extended equalityが使われます(extended equalityについては、Part1を参照)。

(fact 
  (f "hello" "world") => 13
  (provided
    (g #"hello.*world") => 12    ; #"hello.*world"にマッチするような文字列を引数として
                                 ; gが呼ばれるはず。
    (g (roughly 5.0 0.01)) => 89 ; 5.0±0.01の数値を引数として
                                 ; gが呼ばれるはず。
    (g anything) => 90))         ; 任意の値1つを引数としてgが呼ばれるはず。
  • ただし、引数が組み込みchecker以外の関数だったら、extended equalityは発動しない
  • なぜなら、関数型プログラミングでは引数に関数が指定されることが珍しくないから
  • extended equalityしたければ、as-checkerで、checkerだと明示する
(fact
  ...
  (provided
    (g even?) => true                 ; even?を引数としてgが呼ばれる。
    (g (as-checker even?)) => true))  ; 奇数値を引数としてgが呼ばれる。
  • もし自作関数を、prerequisiteで使うcheckerとして定義したいなら、defnの代わりにdefcheckerを使えば良い
  • そのような関数を無名関数として定義したいなら、fnの代わりにcheckerを使えば良い

prerequisiteの回数指定

デフォルトでは、指定したprerequisiteは、少なくとも1回は呼ばれる必要があります(呼ばれなければ、そのテストはNG)。オプションで、呼ばれる回数の期待値を指定できます。

(provided (f 5) => 50)                   ; 1回以上呼ばれればOK。
(provided (f 5) => 50 :times 2)          ; ピッタリ2回呼ばれたらOK。
(provided
  (f 5) => 50 :times [2 3]               ; 2~3回。
  (f 4) => 40 :times (range 3 33))       ; 3~32回。
(provided
  (f 5) => 50 :times (range))            ; 何回でも(0回でも)OK。
(provided
  (f 1) => 1 :times even?))              ; 奇数回ならOK。
(provided
  (f anything) => anything :times 0))    ; 呼ばれたらNG。

prerequisiteのチェイン

サブ関数呼び出しがネストするケースも記述することができます。

(unfinished happens-first happens-second)

(defn function-under-test [n]
  (inc (happens-second (happens-first 1 n))))

(fact
  (function-under-test 5) => 101
  (provided
    (happens-second (happens-first 1 5)) => 100))

参照透過でないサブ関数

サブ関数が参照透過でない場合、同じ引数で呼んでも違う結果を返すかもしれません。その場合、prerequisiteには、返す値をシーケンスで指定することができます。

(fact 
  (converger 2) => (roughly 0.2)
  (provided
    (rand 1.0) =streams=> [0.1 0.3]))  ; =streams=>により、毎回、返す値が異なることを表明。
                                       ; 0.1、0.3の順に返す。
                                       ; 返す値をrangeとかで作ってもOK。

サブ関数がProtocolの場合

Protocolを実装した関数をprerequisiteに指定することはできません。そのような関数は、ClojureではなくJVMから直接呼ばれるため、providedのメカニズムが効かないらしいです。

回避策として、defrecordの代わりにdefrecord-openlyを使う方法が提供されています。

  • midje.open-protocolsをuseする
  • defrecordの代わりにdefrecord-openlyを使う

出荷用のバイナリをコンパイルするときに、midje.sweet/*include-midje-checks*をfalseにしておけば、defrecord-openlyはdefrecordと同じものになるようです。

deftypeについても、同様にdeftype-openlyを使うことで、prerequisiteに指定することができるようになります。

サブ関数がprivateな場合

testable-privatesやexpose-testablesによりexposeしたprivate関数を、prerequisiteに指定することはできません。private関数をprerequisiteに指定したい場合は、名前空間名と共にvarクオートする必要があります。

(fact
  (counter 4) => 8
  (provided
    (#'scratch.core/super-secret-function 4) => 4))

複数のcheckableに共通するprerequisite

(fact
  (prerequisite (y-handler 1) => 2)     ; サブ関数y-handlerは、fact内の全てのcheckableに共通。
                                        ; prerequisitesでもOK。

  (function-under-test 1 1) => 3
  (provided
    (x-handler 1) => 1)
    
  (function-under-test 8 1) => 1
  (provided
    (x-handler 8) => -1))
  • prerequisite(またはprerequisites)フォームで指定したサブ関数は、1度も呼ばれなくてOK
  • prerequisite(またはprerequisites)フォームがネストした状況で、複数のprerequisiteにマッチした場合は、最も内側で、最も最近(最も後に)定義されたprerequisiteが採用される

Partialなprerequisite

仮にサブ関数が実装済みの場合でも、providedで指定したprerequisiteは作動します。もしサブ関数が、ある引数では動くけど、別の引数では動かないような状態(未実装とかバグとか)だった場合、prerequisiteには動かないケースだけを記述し、動くケースについては本物のサブ関数を使いたいでしょう。しかしデフォルトでは、prerequisiteに記述されたサブ関数が呼ばれ、引数がどのprerequisiteにもマッチしない場合、テストはNGとなります。ここでNGとせずに、本物のサブ関数へ流したい場合は、コンフィグのpartial-prerequisitesオプションをtrueに設定します。

Midjeのコンフィグ

コンフィグ可能な項目は、以下の通りです。

  • print-level(出力レベル)
  • emitter(出力フォーマット)
  • fact-filter
  • check-after-creation(ロード直後にチェックするかどうか)
  • visible-deprecation(deprecatedな機能の使用時にwarningを出すかどうか)
  • partial-prerequisites(想定外のスタブ呼び出し時に、本物へfall throughするかどうか)

コンフィグオプションは、コンフィグファイル(.midje.clj)、leinのオプション、またはchange-defaults関数などを使って指定することができます。詳しくは公式ドキュメントを参照して下さい。

また、テストコードを製品に入れないためには、コンパイル時に、midje.sweet/*include-midje-checks*をfalseに設定すると良いようです(☆alter-var-rootを使う?)。

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