---
title: Property-Based Testing with test.check and clojure.spec: ClojureでPBTに(再)入門しよう
tags:  #関数型プログラミング #プロパティベーステスト #clojure  
author: [Kent OHASHI](https://convert.docswell.com/user/lagenorhynque)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/DEYZKKN87M.jpg?width=480
description: Clojureでプロパティベーステストに(再)入門しよう!
published: January 29, 26
canonical: https://convert.docswell.com/s/lagenorhynque/ZX6L1X-property-based-testing-with-test-check-and-clojure-spec
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/DEYZKKN87M.jpg)

Property-Based Testing with
test.check and clojure.spec
で に 再 入門しよう
Clojure PBT ( )
#lispmeetup
1


# Page. 2

![Page Image](https://bcdn.docswell.com/page/VJN3RRX9J8.jpg)

🐬カマイルカ
lagénorhynque
株式会社スマートラウンドのシニアエンジニア
スタートアップと投資家のやり取りを効率化する
データ管理プラットフォームを開発している
技術スタック: Kotlin/Ktor &amp; TypeScript/Vue.js
Server-Side Kotlin Meetupの運営にも協力
関数型プログラミング(言語)とLispの熱烈な愛好者
(特に) ClojureとHaskellがエレガントで好き
関数型まつり2026の運営スタッフ(座長のひとり)
2


# Page. 3

![Page Image](https://bcdn.docswell.com/page/YE99MM83E3.jpg)

プロパティベーステストとは
の基本
の実践
1.
2. test.check + clojure.spec
3. test.check + clojure.spec
3


# Page. 4

![Page Image](https://bcdn.docswell.com/page/GE89LL8L7D.jpg)

1.
プロパティベーステストとは
4


# Page. 5

![Page Image](https://bcdn.docswell.com/page/LELWLLRQJR.jpg)

プロパティベーステスト
(property-based testing, PBT)
入力と予想結果について具体例を挙げるテスト
(example-based testing, EBT)に対して、
「プロパティ」(任意の入力について成り立つ性質)
を定義してランダム生成値で試すテスト
a.k.a. generative testing (生成的テスト)
🐬&lt;
関数型言語の入門書で紹介されることが多い印象
保証の度合いと実装コスト: EBT &lt; PBT &lt; 証明
5


# Page. 6

![Page Image](https://bcdn.docswell.com/page/4JM9MMPKEW.jpg)

のためのライブラリ
実践のためには専用のライブラリが必要
標準的なジェネレーター(generator, arbitrary)
ジェネレーターを組み合わせるコンビネーター
実行してエラー時の入力を収縮(shrink)する機構
元祖といえるのがHaskellのQuickCheck
関数型言語を中心に多くの言語に移植されている
🐬&lt; 現在の仕事でJS/TSのfast-check、Java/Kotlinの
jqwikをよく利用している
PBT
6


# Page. 7

![Page Image](https://bcdn.docswell.com/page/PJRGVV16E9.jpg)

参考] QuickCheckのテストコードと実行結果の例
[
import Test.QuickCheck
関数のプロパティ 任意のリストを 回
すると元に戻る
-- reverse
:
2 reverse
prop_reverse :: [Int] -&gt; Bool
prop_reverse xs = reverse (reverse xs) == xs
-- ghci (Haskell REPL)
プロパティをテストして結果を表示する関数
-- quickCheck:
&gt;&gt;&gt; quickCheck prop_reverse
+++ OK, passed 100 tests.
※
公式ドキュメントのTest.QuickCheckより
7


# Page. 8

![Page Image](https://bcdn.docswell.com/page/PEXXZZYDEX.jpg)

貴重な解説書として『実践プロパティベーステスト
― PropErとErlang/Elixirではじめよう』
🐬&lt; 最近、社内勉強会として読書会を始めた💪
8


# Page. 9

![Page Image](https://bcdn.docswell.com/page/3EKW883DED.jpg)

の
2. test.check + clojure.spec
基本
9


# Page. 10

![Page Image](https://bcdn.docswell.com/page/L73122GPJ5.jpg)

test.check
準標準ライブラリ(Clojure contrib)のひとつ
QuickCheckのClojure版
(require &#039;[clojure.test.check.generators :as gen]
&#039;[clojure.test.check.properties :as prop])
user&gt; (clojure.test.check/quick-check 100
(prop/for-all [xs (gen/list gen/large-integer)]
(= xs (-&gt;&gt; xs reverse reverse))))
{:result true,
:pass? true,
;
(pass)
:num-tests 100, ;
:time-elapsed-ms 13,
:seed 1769528527433}
テストの成否
試行回数
10


# Page. 11

![Page Image](https://bcdn.docswell.com/page/87DXZZ13EG.jpg)

テストがfailしたときの結果データ
user&gt; (clojure.test.check/quick-check 100
(prop/for-all [xs (gen/list gen/large-integer)]
;; 2
reverse
(= xs (-&gt;&gt; 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)]}, ;
:failed-after-ms 2,
:num-tests 4,
;
:seed 1769529199754,
:fail [(-1 1)],
; fail
:result false,
:result-data nil,
:failing-size 3,
; fail
:pass? false}
;
単純化されたfailする入力値
試行回数
時の実際の入力値
時のsize値(0, 1, 2, 3で4回目)
テストの成否(fail)
11


# Page. 12

![Page Image](https://bcdn.docswell.com/page/VJPPDDQPJ8.jpg)

基本構文(マクロ)
プロパティ: for-all
clojure.test連携: defspec
user&gt; (tc/defspec reverse-test
(prop/for-all [xs (gen/list gen/large-integer)]
(= xs (-&gt;&gt; xs reverse reverse))))
#&#039;user/reverse-test
user&gt; (clojure.test/run-test reverse-test)
Testing user
{:result true, :num-tests 100, :seed 1769532780193,
:time-elapsed-ms 15, :test-var &quot;reverse-test&quot;}
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
{:test 1, :pass 1, :fail 0, :error 0, :type :summary}
12


# Page. 13

![Page Image](https://bcdn.docswell.com/page/2EV2115VEQ.jpg)

標準提供の主なジェネレーター
数値: small-integer, large-integer, double
文字: char, char-alphanumeric, char-ascii
文字列: string, string-alphanumeric
コレクション: list, vector, tuple, set
user&gt; (gen/sample (gen/vector gen/double) 5)
([] [] [] [3.25 -0.75] [0.625 2.0 2.0])
user&gt; (gen/sample (gen/tuple gen/string gen/small-integer) 5)
([&quot;&quot; 0] [&quot;&quot; 0] [&quot;\&quot;&quot; 2] [&quot;&quot; 1] [&quot;&quot; 3])
user&gt; (gen/sample (gen/set gen/char-ascii) 5)
(#{} #{\%} #{\space \o} #{\G} #{\h \m \R})
13


# Page. 14

![Page Image](https://bcdn.docswell.com/page/57GR2261EL.jpg)

選択: choose, elements, one-of, frequency
user&gt; (gen/sample (gen/choose 0 100))
(66 99 38 63 86 64 34 13 74 87)
user&gt; (gen/sample (gen/elements #{:a :b :c :d}))
(:b :d :b :a :a :b :a :d :a :d)
user&gt; (gen/sample (gen/one-of [gen/char gen/string]))
(\Ê \h &quot;¬¿&quot; &quot;}&quot; &quot;C4g%&quot; &quot;&quot; \ú &quot;½&quot; &quot;&quot; &quot;)ÏÚ¼Ub&quot;)
user&gt; (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


# Page. 15

![Page Image](https://bcdn.docswell.com/page/4EQVPP4NJP.jpg)

ジェネレーターに対する主なコンビネーター
such-that (filter 関数相当)
fmap (map 関数相当)
cf. HaskellのFunctor 型クラス
user&gt; (gen/sample (gen/such-that seq
gen/string-alphanumeric))
(&quot;4&quot; &quot;o&quot; &quot;NVV&quot; &quot;T3&quot; &quot;YfqJ&quot; &quot;JH87x&quot; &quot;B4496&quot; &quot;6PZ&quot; &quot;1&quot; &quot;4rsOz&quot;)
user&gt; (gen/sample (gen/fmap #(* % %)
gen/large-integer))
(0 0 1 9 16 1 0 169 1 49)
15


# Page. 16

![Page Image](https://bcdn.docswell.com/page/KJ4MYYQ3E1.jpg)

関数相当)
cf. HaskellのMonad 型クラス
let (return, bind, fmap に対する糖衣構文)
cf. Haskellのdo記法
return, bind (mapcat
user&gt; (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&gt; (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


# Page. 17

![Page Image](https://bcdn.docswell.com/page/LE1866PZ7G.jpg)

マクロ展開すると bind (と return)の連鎖になる
user&gt; (clojure.walk/macroexpand-all
&#039;(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


# Page. 18

![Page Image](https://bcdn.docswell.com/page/GEWZWW26E2.jpg)

clojure.spec
標準ライブラリ: Clojure 1.9 (2017年12月)〜
ただし、2026年1月現在も alpha 😂
述語(predicate)ベースの仕様記述ライブラリ
一種のcontract system
cf. Racketのcontract
18


# Page. 19

![Page Image](https://bcdn.docswell.com/page/47Z155DRJ3.jpg)

公式ドキュメントのclojure.specのRationaleでは
Writing a spec should enable
automatic:
Validation
Error reporting
Destructuring
Instrumentation
Test-data generation
Generative test generation
見落とされがちな(?)この点が非常に強力😏
19


# Page. 20

![Page Image](https://bcdn.docswell.com/page/YJ6L9911EV.jpg)

冒頭の例をclojure.specによるジェネレーター実装に
書き換えると
(require &#039;[clojure.spec.alpha :as s]
&#039;[clojure.spec.gen.alpha :as sgen]
&#039;[clojure.test.check.properties :as prop])
user&gt; (clojure.test.check/quick-check 100
(prop/for-all [xs (s/gen (s/coll-of int?
:kind list?))]
(= xs (-&gt;&gt; xs reverse reverse))))
{:result true,
:pass? true,
:num-tests 100,
:time-elapsed-ms 13,
:seed 1769528682166}
20


# Page. 21

![Page Image](https://bcdn.docswell.com/page/GJ51NN4L74.jpg)

の
のジェネレーター
clojure.spec spec → test.check
標準ライブラリの述語
;;
user&gt; (sgen/sample (s/gen string?))
(&quot;&quot; &quot;t&quot; &quot;4&quot; &quot;&quot; &quot;Tk6&quot; &quot;a21&quot; &quot;Lj&quot; &quot;&quot; &quot;iq7m&quot; &quot;&quot;)
;;
user&gt; (sgen/sample (s/gen #{:α :β :γ :δ}) 5)
(:β :α :β :α :β)
;;
spec
user&gt; (sgen/sample (s/gen (s/cat :s string? :n int?)) 5)
((&quot;&quot; 0) (&quot;O&quot; -1) (&quot;P9&quot; -1) (&quot;&quot; 1) (&quot;C2n&quot; 0))
セット
シーケンスの
エンティティとしての マップの
;; (
)
spec
user&gt; (s/def ::foo (s/and string? #(&lt;= (count %) 10)))
:user/foo
user&gt; (s/def ::bar nat-int?)
:user/bar
user&gt; (sgen/sample (s/gen (s/keys :req-un [::foo ::bar])) 3)
({:foo &quot;&quot;, :bar 1} {:foo &quot;F&quot;, :bar 0} {:foo &quot;6&quot;, :bar 0})
21


# Page. 22

![Page Image](https://bcdn.docswell.com/page/LE3122GD75.jpg)

⚠️ 利用上の主な注意点
clojure.spec.gen.alpha (sgen)と
clojure.test.check.generators (gen)の関係
sgenからgenの大多数のオペレーターが使えるが
遅延ロードされている
→ src配下のgen参照を避けるとtest.checkは
dev/test dependenciesに限定できる
述語と同名のtest.checkジェネレーターがあっても
完全に同じ実装とは限らない
sgenのgen-builtinsで対応関係が確認できる
s/and を利用する場合には標準の述語をベースに
無条件でジェネレーターになるわけではない
22


# Page. 23

![Page Image](https://bcdn.docswell.com/page/8EDXZZ12JG.jpg)

の
3. test.check + clojure.spec
実践
23


# Page. 24

![Page Image](https://bcdn.docswell.com/page/V7PPDDQL78.jpg)

『実践プロパティベーステスト』の例題/演習問題
標準ライブラリ関数
に関するプロパティのテスト
;;
range
(tc/defspec range-test 1000
(prop/for-all [start (s/gen int?)
len (s/gen (s/and nat-int?
#(&lt;= % 10000)))]
(let [coll (range start (+ start len))]
(and (= len
;
(count coll))
(increments? coll))))) ;
1
長さは想定通りか
要素は ずつ増えているか
;; increments? (テスト用のヘルパー関数)の実装は省略
cf.
🐬のリポジトリ: lagenorhynque/property-based-testing-withproper-erlang-and-elixir
24


# Page. 25

![Page Image](https://bcdn.docswell.com/page/2JV21156JQ.jpg)

🐬が実務で書いたPBTの例(1): 現在価値の計算
将来価値
現在価値 = ( 1 + 割引率 ) 年数
テスト対象 現在価値の計算関数
;;
:
(defn present-value [^BigDecimal future-value rate years]
(.divide future-value
(bigdec (math/pow (+ 1 rate) years))
2
RoundingMode/DOWN))
user&gt; (present-value 1000000M
0.05
5)
783526.16M
※
将来価値 100万円
割引率 5%
年数 5年
現在価値 約78万円
;
;
;
; =&gt;
実際のスタック: TypeScript + fast-check, Kotlin + jqwik
25


# Page. 26

![Page Image](https://bcdn.docswell.com/page/5EGR2262JL.jpg)

(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?
#(&lt;= 0 % 1)))]
;;
0
(= future-value
(present-value future-value rate 0))))
年数が 年のとき、割引率にかかわらず現在価値は将来価値に一致する
26


# Page. 27

![Page Image](https://bcdn.docswell.com/page/4JQVPP497P.jpg)

🐬が実務で書いたPBTの例(2): 証券コードの形式
(def ^:private allowed-letters
;; B, E, I, O, Q, V, Z
&quot;(?![BEIOQVZ])[A-Z]&quot;)
は除外文字
(def security-code-regex
&quot;
:
- 1300 9999
4
- 2
4
(
)
&quot;
(re-pattern (str \^
&quot;(?:1(?:[3-9]|&quot; allowed-letters &quot;)&quot;
&quot;|[2-9](?:[0-9]|&quot; allowed-letters &quot;))&quot;
&quot;[0-9](?:[0-9]|&quot; allowed-letters &quot;)&quot;
\$)))
証券コードの仕様
〜 の範囲の 桁の数字
桁目または 桁目に英大文字 除外文字を除く が入ることがある
user&gt; (re-matches security-code-regex &quot;130A&quot;)
&quot;130A&quot;
user&gt; (re-matches security-code-regex &quot;130B&quot;)
nil
27


# Page. 28

![Page Image](https://bcdn.docswell.com/page/K74MYYLRJ1.jpg)

(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?
#(&lt;= (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&#039; (num-or-letter-gen second)
fourth&#039; (num-or-letter-gen fourth)]
(str first second&#039; third fourth&#039;))))
数字または の文字
の桁
(tc/defspec security-code-regex-test 1000
(prop/for-all [code security-code-like-gen]
;;
(= (nil? (re-find #&quot;[BEIOQVZ]&quot; code))
(some? (re-matches security-code-regex code)))))
証券コードらしい形式の文字列は除外文字を含まなければマッチする
28


# Page. 29

![Page Image](https://bcdn.docswell.com/page/LJ1866LVEG.jpg)

おわりに
😆 PBTという手法の良さ
簡潔なテストコードで膨大なパターンを試せる
想定外の動作を検出できる(かもしれない)
🥹 PBT実践における困難
意味のあるプロパティの発見
安定的かつ効率的なジェネレーターの実装
Clojureでも(他言語でも)ぜひPBTに挑戦し活用しよう!
29


# Page. 30

![Page Image](https://bcdn.docswell.com/page/GJWZWWV3J2.jpg)

Further Reading
clojure/test.check: QuickCheck for Clojure
Introduction to test.check
Clojure - test.check
A Practical Guide to test.check
cf. QuickCheck: Automatic testing of Haskell
programs
Clojure - spec Guide
― PropEr
Erlang/Elixir
lagenorhynque/property-based-testing-withproper-erlang-and-elixir:
Clojure
『実践プロパティベーステスト
と
ではじめよう』
🐬による
実装
30


