Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Property-Based Testing with test.check and cloj...
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Kent OHASHI
January 29, 2026
Programming
0
12
Property-Based Testing with test.check and clojure.spec
Clojureでプロパティベーステストに(再)入門しよう!
Kent OHASHI
January 29, 2026
Tweet
Share
More Decks by Kent OHASHI
See All by Kent OHASHI
🐬の推し本紹介2025: 『コーディングを支える技術 ――成り立ちから学ぶプログラミング作法』
lagenorhynque
0
70
KotlinでミニマルなResult実装による関数型エラーハンドリング
lagenorhynque
0
25
Functional Calisthenics in Kotlin: Kotlinで「関数型エクササイズ」を実践しよう
lagenorhynque
1
240
関数型言語テイスティング: Haskell, Scala, Clojure, Elixirを比べて味わう関数型プログラミングの旨さ
lagenorhynque
1
150
純LISPから考える関数型言語のプリミティブ: Clojure, Elixir, Haskell, Scala
lagenorhynque
1
190
From Scala/Clojure to Kotlin
lagenorhynque
0
81
TDD with RDD: Clojure/LispのREPLで変わる開発体験
lagenorhynque
0
110
🐬の推し本紹介2024: 『脱・日本語なまり 英語(+α)実践音声学』
lagenorhynque
1
140
do Notation Equivalents in JVM languages: Scala, Kotlin, Clojure
lagenorhynque
0
110
Other Decks in Programming
See All in Programming
[AI Engineering Summit Tokyo 2025] LLMは計画業務のゲームチェンジャーか? 最適化業務における活⽤の可能性と限界
terryu16
2
570
Python札幌 LT資料
t3tra
7
1.1k
AI Schema Enrichment for your Oracle AI Database
thatjeffsmith
0
180
Data-Centric Kaggle
isax1015
2
720
AI前提で考えるiOSアプリのモダナイズ設計
yuukiw00w
0
220
re:Invent 2025 トレンドからみる製品開発への AI Agent 活用
yoskoh
0
710
MUSUBIXとは
nahisaho
0
110
SourceGeneratorのススメ
htkym
0
160
re:Invent 2025 のイケてるサービスを紹介する
maroon1st
0
180
Graviton と Nitro と私
maroon1st
0
180
2026年 エンジニアリング自己学習法
yumechi
0
120
MDN Web Docs に日本語翻訳でコントリビュート
ohmori_yusuke
0
620
Featured
See All Featured
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
359
30k
Navigating Team Friction
lara
192
16k
The SEO Collaboration Effect
kristinabergwall1
0
340
How Software Deployment tools have changed in the past 20 years
geshan
0
31k
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
3.9k
Making the Leap to Tech Lead
cromwellryan
135
9.7k
Testing 201, or: Great Expectations
jmmastey
46
8k
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
55
49k
The untapped power of vector embeddings
frankvandijk
1
1.6k
Making Projects Easy
brettharned
120
6.6k
Code Reviewing Like a Champion
maltzj
527
40k
A brief & incomplete history of UX Design for the World Wide Web: 1989–2019
jct
1
290
Transcript
Property-Based Testing with test.check and clojure.spec Clojure でPBT に( 再)
入門しよう #lispmeetup 1
のシニアエンジニア スタートアップと投資家のやり取りを効率化する データ管理プラットフォームを開発している 技術スタック: Kotlin/Ktor & TypeScript/Vue.js の運営にも協力 関数型プログラミング( 言語)
とLisp の熱烈な愛好者 ( 特に) と がエレガントで好き の運営スタッフ( 座長のひとり) lagénorhynque 🐬カマイルカ 株式会社スマートラウンド Server-Side Kotlin Meetup Clojure Haskell 関数型まつり2026 2
1. プロパティベーステストとは 2. test.check + clojure.spec の基本 3. test.check +
clojure.spec の実践 3
1. プロパティベーステストとは 4
プロパティベーステスト (property-based testing, PBT) 入力と予想結果について具体例を挙げるテスト (example-based testing, EBT) に対して、 「プロパティ」(
任意の入力について成り立つ性質) を定義してランダム生成値で試すテスト a.k.a. generative testing ( 生成的テスト) 🐬< 関数型言語の入門書で紹介されることが多い印象 保証の度合いと実装コスト: EBT < PBT < 証明 5
PBT のためのライブラリ 実践のためには専用のライブラリが必要 標準的なジェネレーター(generator, arbitrary) ジェネレーターを組み合わせるコンビネーター 実行してエラー時の入力を収縮(shrink) する機構 元祖といえるのが の
関数型言語を中心に多くの言語に移植されている 🐬< 現在の仕事でJS/TS の 、Java/Kotlin の をよく利用している Haskell QuickCheck fast-check jqwik 6
[ 参考] QuickCheck のテストコードと実行結果の例 ※ 公式ドキュメントの より 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 7
貴重な解説書として 🐬< 最近、社内勉強会として読書会を始めた💪 『実践プロパティベーステスト ― PropEr とErlang/Elixir ではじめよう』 8
2. test.check + clojure.spec の 基本 9
準標準ライブラリ( ) のひとつ QuickCheck のClojure 版 test.check Clojure contrib (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} 10
テストがfail したときの結果データ 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) 11
基本構文( マクロ) プロパティ: 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} 12
標準提供の主なジェネレーター 数値: 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}) 13
選択: 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) 14
ジェネレーターに対する主なコンビネーター such-that (filter 関数相当) fmap (map 関数相当) cf. Haskell のFunctor
型クラス 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) 15
return, bind (mapcat 関数相当) cf. Haskell の let (return, bind,
fmap に対する糖衣構文) cf. Haskell の Monad 型クラス do 記法 user> (gen/sample (gen/let [x gen/small-integer y gen/large-integer] (gen/elements [x y]))) (0 0 1 -1 -2 4 -1 0 -4 7) user> (gen/sample (gen/let [x gen/small-integer y gen/large-integer] {:x x :y y}) 5) ({:x 0, :y -1} {:x -1, :y 0} {:x 1, :y -1} {:x 1, :y -1} {:x 1, :y 6}) 16
マクロ展開すると bind ( と return) の連鎖になる user> (clojure.walk/macroexpand-all '(gen/let [x
gen/small-integer y gen/large-integer] (gen/elements [x y]))) (clojure.test.check.generators/bind gen/small-integer (fn* ([x] (clojure.test.check.generators/bind gen/large-integer (fn* ([y] (let* [val__6616__auto__ (do (gen/elements [x y]))] (if ; ボディがジェネレーターでなければreturn でジェネレーターに (clojure.test.check.generators/generator? val__6616__aut val__6616__auto__ (clojure.test.check.generators/return val__6616__auto__) 17
標準ライブラリ: (2017 年12 月) 〜 ただし、2026 年1 月現在も alpha 😂
述語(predicate) ベースの仕様記述ライブラリ 一種のcontract system cf. の clojure.spec Clojure 1.9 Racket contract 18
公式ドキュメントの では 見落とされがちな(?) この点が非常に強力😏 clojure.spec のRationale Writing a spec should
enable automatic: Validation Error reporting Destructuring Instrumentation Test-data generation Generative test generation 19
冒頭の例をclojure.spec によるジェネレーター実装に 書き換えると (require '[clojure.spec.alpha :as s] '[clojure.spec.gen.alpha :as sgen]
'[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} 20
clojure.spec のspec → test.check のジェネレーター ;; 標準ライブラリの述語 user> (sgen/sample (s/gen
string?)) ("" "t" "4" "" "Tk6" "a21" "Lj" "" "iq7m" "") ;; セット user> (sgen/sample (s/gen #{:α :β :γ :δ}) 5) (:β :α :β :α :β) ;; シーケンスのspec user> (sgen/sample (s/gen (s/cat :s string? :n int?)) 5) (("" 0) ("O" -1) ("P9" -1) ("" 1) ("C2n" 0)) ;; ( エンティティとしての) マップのspec user> (s/def ::foo (s/and string? #(<= (count %) 10))) :user/foo user> (s/def ::bar nat-int?) :user/bar user> (sgen/sample (s/gen (s/keys :req-un [::foo ::bar])) 3) ({:foo "", :bar 1} {:foo "F", :bar 0} {:foo "6", :bar 0}) 21
⚠️ 利用上の主な注意点 (sgen) と (gen) の関係 sgen からgen の大多数のオペレーターが使えるが 遅延ロードされている
→ src 配下のgen 参照を避けるとtest.check は dev/test dependencies に限定できる 述語と同名のtest.check ジェネレーターがあっても 完全に同じ実装とは限らない sgen の で対応関係が確認できる s/and を利用する場合には標準の述語をベースに 無条件でジェネレーターになるわけではない clojure.spec.gen.alpha clojure.test.check.generators gen-builtins 22
3. test.check + clojure.spec の 実践 23
『実践プロパティベーステスト』の例題/ 演習問題 cf. 🐬のリポジトリ: ;; 標準ライブラリ関数 range に関するプロパティのテスト (tc/defspec range-test
1000 (prop/for-all [start (s/gen int?) len (s/gen (s/and nat-int? #(<= % 10000)))] (let [coll (range start (+ start len))] (and (= len ; 長さは想定通りか (count coll)) (increments? coll))))) ; 要素は1 ずつ増えているか ;; increments? ( テスト用のヘルパー関数) の実装は省略 lagenorhynque/property-based-testing-with- proper-erlang-and-elixir 24
🐬が実務で書いたPBT の例(1): の計算 現在価値= 将来価値 (1 + 割引率) 年数 ※
実際のスタック: TypeScript + , Kotlin + 現在価値 ;; テスト対象: 現在価値の計算関数 (defn present-value [^BigDecimal future-value rate years] (.divide future-value (bigdec (math/pow (+ 1 rate) years)) 2 RoundingMode/DOWN)) user> (present-value 1000000M ; 将来価値 100 万円 0.05 ; 割引率 5% 5) ; 年数 5 年 783526.16M ; => 現在価値 約78 万円 fast-check jqwik 25
(tc/defspec present-value-rate-zero-test 1000 (prop/for-all [future-value (sgen/fmap bigdec (s/gen nat-int?)) years
(s/gen nat-int?)] ;; 割引率が0% のとき、年数にかかわらず現在価値は将来価値に一致する (= future-value (present-value future-value 0 years)))) (tc/defspec present-value-years-zero-test 1000 (prop/for-all [future-value (sgen/fmap bigdec (s/gen nat-int?)) rate (s/gen (s/and double? #(<= 0 % 1)))] ;; 年数が0 年のとき、割引率にかかわらず現在価値は将来価値に一致する (= future-value (present-value future-value rate 0)))) 26
🐬が実務で書いたPBT の例(2): の形式 証券コード (def ^:private allowed-letters ;; B, E,
I, O, Q, V, Z は除外文字 "(?![BEIOQVZ])[A-Z]") (def security-code-regex " 証券コードの仕様: - 1300 〜9999 の範囲の4 桁の数字 - 2 桁目または4 桁目に英大文字( 除外文字を除く) が入ることがある" (re-pattern (str \^ "(?:1(?:[3-9]|" allowed-letters ")" "|[2-9](?:[0-9]|" allowed-letters "))" "[0-9](?:[0-9]|" allowed-letters ")" \$))) user> (re-matches security-code-regex "130A") "130A" user> (re-matches security-code-regex "130B") nil 27
(def ^:private security-code-like-gen (let [num-or-letter-gen ; 数字またはA-Z の文字 (fn [num]
(sgen/one-of [(sgen/return num) (s/gen (s/and char? #(<= (int \A) (int %) (int \Z))))]))] (gen/let [[first second third fourth] ; 1300-9999 の4 桁 (sgen/fmap str (s/gen (s/int-in 1300 (inc 9999)))) second' (num-or-letter-gen second) fourth' (num-or-letter-gen fourth)] (str first second' third fourth')))) (tc/defspec security-code-regex-test 1000 (prop/for-all [code security-code-like-gen] ;; 証券コードらしい形式の文字列は除外文字を含まなければマッチする (= (nil? (re-find #"[BEIOQVZ]" code)) (some? (re-matches security-code-regex code))))) 28
おわりに 😆 PBT という手法の良さ 簡潔なテストコードで膨大なパターンを試せる 想定外の動作を検出できる( かもしれない) 🥹 PBT 実践における困難
意味のあるプロパティの発見 安定的かつ効率的なジェネレーターの実装 Clojure でも( 他言語でも) ぜひPBT に挑戦し活用しよう! 29
Further Reading cf. : 🐬によるClojure 実装 clojure/test.check: QuickCheck for Clojure
Introduction to test.check Clojure - test.check A Practical Guide to test.check QuickCheck: Automatic testing of Haskell programs Clojure - spec Guide 『実践プロパティベーステスト ― PropEr と Erlang/Elixir ではじめよう』 lagenorhynque/property-based-testing-with- proper-erlang-and-elixir 30