- 株式会社スマートラウンドのシニアエンジニア
- スタートアップと投資家のやり取りを効率化するデータ管理プラットフォームを開発している
- 技術スタック: Kotlin/Ktor & TypeScript/Vue.js
- Server-Side Kotlin Meetupの運営にも協力
- スタートアップと投資家のやり取りを効率化するデータ管理プラットフォームを開発している
- 関数型プログラミング(言語)とLispの熱烈な愛好者
- 関数型まつり2026の運営スタッフ(座長のひとり)
-
プロパティベーステストとは
-
test.checkとclojure.specの基本
-
test.check + clojure.specでの実践
-
入力と予想結果について具体例を挙げるテスト(example-based testing, EBT)に対して、
「プロパティ」(任意の入力について成り立つ性質)を定義してランダム生成値で試すテスト -
a.k.a. generative testing (生成的テスト)
-
🐬<
-
関数型言語の入門書で紹介されることが多い印象
-
保証の度合いと実装コスト: EBT < PBT < 証明
-
-
実践のためには専用のライブラリが必要
-
標準的なジェネレーター(generator, arbitrary)
-
ジェネレーターを組み合わせるコンビネーター
-
実行してエラー時の入力を収縮(shrink)する機構
-
-
元祖といえるのがHaskellのQuickCheck
- 関数型言語を中心に多くの言語に移植されている
-
🐬< 現在の仕事でJS/TSのfast-check、Java/Kotlinのjqwikをよく利用している
import Test.QuickCheck
-- reverse関数のプロパティ: 任意のリストを2回reverseすると元に戻る
prop_reverse :: [Int] -> Bool
prop_reverse xs = reverse (reverse xs) == xs-- ghci (Haskell REPL)
-- quickCheck: プロパティをテストして結果を表示する関数
>>> quickCheck prop_reverse
+++ OK, passed 100 tests.※公式ドキュメントのTest.QuickCheckより
🐬< 最近、社内勉強会として読書会を始めた💪
-
準標準ライブラリ(Clojure contrib)のひとつ
-
QuickCheckのClojure版
(require '[clojure.test.check.generators :as gen]
'[clojure.test.check.properties :as prop])user> (clojure.test.check/quick-check 100
(prop/for-all [xs (gen/list gen/large-integer)]
(= xs (->> xs reverse reverse))))
{:result true,
:pass? true, ; テストの成否(pass)
:num-tests 100, ; 試行回数
:time-elapsed-ms 13,
:seed 1769528527433}user> (clojure.test.check/quick-check 100
(prop/for-all [xs (gen/list gen/large-integer)]
;; 2回目のreverseを敢えてコメントアウト
(= xs (->> xs reverse #_reverse)))){:shrunk ; 収縮(shrink)された結果
{:total-nodes-visited 7,
:depth 1,
:pass? false,
:result false,
:result-data nil,
:time-shrinking-ms 6,
:smallest [(0 1)]}, ; 単純化されたfailする入力値
:failed-after-ms 2,
:num-tests 4, ; 試行回数
:seed 1769529199754,
:fail [(-1 1)], ; fail時の実際の入力値
:result false,
:result-data nil,
:failing-size 3, ; fail時のsize値(0, 1, 2, 3で4回目)
:pass? false} ; テストの成否(fail)-
プロパティ:
for-all -
clojure.test連携:
defspec
user> (tc/defspec reverse-test
(prop/for-all [xs (gen/list gen/large-integer)]
(= xs (->> xs reverse reverse))))
#'user/reverse-test
user> (clojure.test/run-test reverse-test)
Testing user
{:result true, :num-tests 100, :seed 1769532780193,
:time-elapsed-ms 15, :test-var "reverse-test"}
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
{:test 1, :pass 1, :fail 0, :error 0, :type :summary}-
数値:
small-integer,large-integer,double -
文字:
char,char-alphanumeric,char-ascii -
文字列:
string,string-alphanumeric -
コレクション:
list,vector,tuple,set
user> (gen/sample (gen/vector gen/double) 5)
([] [] [] [3.25 -0.75] [0.625 2.0 2.0])
user> (gen/sample (gen/tuple gen/string gen/small-integer) 5)
(["" 0] ["" 0] ["\"" 2] ["" 1] ["" 3])
user> (gen/sample (gen/set gen/char-ascii) 5)
(#{} #{\%} #{\space \o} #{\G} #{\h \m \R})- 選択:
choose,elements,one-of,frequency
user> (gen/sample (gen/choose 0 100))
(66 99 38 63 86 64 34 13 74 87)
user> (gen/sample (gen/elements #{:a :b :c :d}))
(:b :d :b :a :a :b :a :d :a :d)
user> (gen/sample (gen/one-of [gen/char gen/string]))
(\Ê \h "¬¿" "}" "C4g%" "" \ú "½" "" ")ÏÚ¼Ub")
user> (gen/sample (gen/frequency [[1 gen/large-integer]
[3 gen/double]]))
(0.5 1.0 -2.0 -2.0 1 -1.03125 0 -0.5625 -1.0546875 -1)-
such-that(filter関数相当) -
fmap(map関数相当)- cf. Haskellの
Functor型クラス
- cf. Haskellの
user> (gen/sample (gen/such-that seq
gen/string-alphanumeric))
("4" "o" "NVV" "T3" "YfqJ" "JH87x" "B4496" "6PZ" "1" "4rsOz")
user> (gen/sample (gen/fmap #(* % %)
gen/large-integer))
(0 0 1 9 16 1 0 169 1 49)-
return,bind(mapcat関数相当)- cf. Haskellの
Monad型クラス
- cf. Haskellの
-
let(return,bind,fmapに対する糖衣構文)- cf. Haskellのdo記法
;; TODO-
標準ライブラリ: Clojure 1.9 (2017年12月)〜
- ただし、2026年1月現在も alpha 😂
-
述語(predicate)ベースの仕様記述ライブラリ
公式ドキュメントのclojure.specのRationaleでは
Writing a spec should enable automatic:
- Validation
- Error reporting
- Destructuring
- Instrumentation
- Test-data generation
- Generative test generation
見落とされがちな(?)この点が非常に強力😏
(require '[clojure.spec.alpha :as s]
'[clojure.test.check.properties :as prop])user> (clojure.test.check/quick-check 100
(prop/for-all [xs (s/gen (s/coll-of int?
:kind list?))]
(= xs (->> xs reverse reverse))))
{:result true,
:pass? true,
:num-tests 100,
:time-elapsed-ms 13,
:seed 1769528682166};; TODO-
述語と同名のtest.checkジェネレーターが存在しても完全に同じ実装とは限らない
-
clojure.spec.gen.alpha(sgen)とclojure.test.check.generators(gen)の関係 -
s/andを利用する場合にはジェネレーター実装があらかじめ用意されている述語をベースにする(またはジェネレーターを独自実装)
;; TODO

