clojure.specを開発やテストで活用する
clojure.specは僕が改めて書かなくてもathosさんやカマイルカさんがいろいろと素晴らしい発表をしているので、改めて僕が書く必要はないと思っていたんだけど、Clojureそのものに不慣れな人たちはまだ使い方が伝わっていないようだったので簡単にどう活用すればいいか書いてみようと思う。
clojure.specは大きく3つの使い方がある
ドキュメントの内容を補完する
開発やテストのときにspecによるチェックを行う
プロパティベーステスト
ひとつ目は特に意識しなくてもいいんだけど、ふたつ目とみっつ目は意識して行わない限り使うことができない。ひとつずつ簡単に説明していくことにします。
ドキュメントの内容を補完する
これはとても分かりやすい。例えば、以下のようなwrap-keyword-paramsというRingミドルウェアがあったとする。
code:clojure
(ns demo.core
(:require clojure.spec.alpha :as s))
(defn wrap-keyword-params
(handler
(wrap-keyword-params handler nil))
(handler exclude-regex
(letfn [(f params
(reduce-kv (fn m k v
(let [k (if (and (regex? exclude-regex)
(re-matches exclude-regex k))
k
(keyword k))]
(assoc m k v)))
{}
params))]
(fn -wrap-keyword-params req
(handler (update req :params f))))))
このとき、このRingミドルウェアのドキュメントは以下のような表示になります。
code:clojure
dev> (require 'demo.core)
;;=> nil
dev> (doc demo.core/wrap-keyword-params)
-------------------------
demo.core/wrap-keyword-params
(handler handler exclude-regex)
;;=> nil
これに以下のようなspecを書いてあげる。
code:clojure
(defn- regex? x
(instance? java.util.regex.Pattern x))
;; sはclojure.spec.alphaのエイリアス
(s/fdef wrap-keyword-params
:args (s/cat :handler fn? :regex (s/? regex?))
:ret fn?)
こうするとドキュメントの表示が以下のようにspecを考慮したものとなる。
code:clojure
dev> (doc demo.core/wrap-keyword-params)
-------------------------
demo.core/wrap-keyword-params
(handler handler exclude-regex)
Spec
args: (cat :handler fn? :regex (? regex?))
ret: fn?
;;=> nil
開発やテストのときにspecによるチェックを行う
clojure.specはclojure.spec.test.alpha/instrumentという関数を実行することで、その後にspecを定義してある関数を実行するたびにspecの通りの引数や戻り値になっているかをチェックしてくれる。
code:clojure
;; instrument実行前
dev> (demo.core/wrap-keyword-params identity "")
;;=> #functiondemo.core/wrap-keyword-params/-wrap-keyword-params--13623
;; instrument実行後
dev> (require 'clojure.spec.test.alpha)
;;=> nil
dev> (clojure.spec.test.alpha/instrument)
;;=> demo.core/wrap-keyword-params
dev> (demo.core/wrap-keyword-params identity "")
ExceptionInfo Call to #'demo.core/wrap-keyword-params did not conform to spec:
In: 1 val: "" fails at: :args :regex predicate: regex?
clojure.core/ex-info (core.clj:4739)
で、これをどうやって実行すると良いのかという話。いくつかの選択肢があるので、パッと書き出してみる。
シンプルにREPL起動後に手動で毎回実行する
REPL起動時に必ず読み込まれるネームスペースでinstrumentを実行する
テスト実行スクリプトでテスト実行前にinstrumentを実行する
CIDERの機能でリフレッシュ時にフックする
たぶんこんなもんかな。REPL起動後に手動でinstrumentを実行するのは特に説明しなくても大丈夫だと思います。REPL起動時に必ず読み込まれるネームスペースというのは、userネームスペース、あるいはDuctやStuart Sierraのreloadedに影響を受けていればdevネームスペースだったりすると思うんですが、このネームスペースでinstrumentを実行するというもの。例えば以下のように。
code:clojure
(ns dev
(:require clojure.spec.test.alpha :as stest))
(stest/instrument)
これは例なので実際にはもっといろいろなネームスペースがrequireされているでしょうし、何よりspecを書いてあるネームスペースをある程度読み込んでいることが前提です。そうでないとinstrumentはspecを認識できないので。
テスト実行スクリプトはLeiningenを利用している場合はあまり意識することがないですが、Clojure CLIを利用する場合などは分かりやすいと思う。テスト対象のネームスペースをすべて読み込んだ後(手動/自動問わず)に、instrumentを実行するとテスト実行時にspec違反しているものがあればエラーになってコケるようになる。そのため、テスト実行スクリプトでなくても、必ずテストの前に読み込まれるネームスペースであれば同様のことを行うことが可能です。
最後にCIDERを利用している場合、cider-ns-refreshの後にinstrumentを実行することができる。詳しくは以下を参照してください。
cider-ns-refreshのタイミングでシステムを再起動したい
プロパティベーステスト
clojure.spec.test.alpha/checkを使って自動プロパティベーステストを実行するか、clojure.test.check.generatorsネームスペースあたりを使ってプロパティベーステストを書くことができます。 このあたりは他の資料に詳しいので、参考にしてみてください。
https://speakerdeck.com/athos/clojure-dot-specfalsehua-1?slide=32
https://www.slideshare.net/KentOhashi/spectacular-future-with-clojurespec