Skip to content

Instantly share code, notes, and snippets.

@lagenorhynque
Last active January 27, 2026 17:30
Show Gist options
  • Select an option

  • Save lagenorhynque/bf99fc6fe23cc8a5a1b65f120fef622d to your computer and use it in GitHub Desktop.

Select an option

Save lagenorhynque/bf99fc6fe23cc8a5a1b65f120fef622d to your computer and use it in GitHub Desktop.
Property-Based Testing with test.check and clojure.spec: ClojureでPBTに(再)入門しよう
slides:
charset: utf-8
theme: night
highlight_theme: monokai-sublime
separator_vertical: ^\s*----\s*$
revealjs:
transition: convex
plugins:
- extra_css:
- https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css

Property-Based Testing with test.check and clojure.spec

ClojureでPBTに(再)入門しよう


x icon


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

  2. test.checkとclojure.specの基本

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


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


プロパティベーステスト
(property-based testing, PBT)

  • 入力と予想結果について具体例を挙げるテスト(example-based testing, EBT)に対して、
    「プロパティ」(任意の入力について成り立つ性質)を定義してランダム生成値で試すテスト

  • a.k.a. generative testing (生成的テスト)

  • 🐬<

    • 関数型言語の入門書で紹介されることが多い印象

    • 保証の度合いと実装コスト: EBT < PBT < 証明


PBTのためのライブラリ

  • 実践のためには専用のライブラリが必要

    • 標準的なジェネレーター(generator, arbitrary)

    • ジェネレーターを組み合わせるコンビネーター

    • 実行してエラー時の入力を収縮(shrink)する機構

  • 元祖といえるのがHaskellQuickCheck

    • 関数型言語を中心に多くの言語に移植されている
  • 🐬< 現在の仕事でJS/TSのfast-check、Java/Kotlinのjqwikをよく利用している


[参考] 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より


pbt book

🐬< 最近、社内勉強会として読書会を始めた💪


2. test.checkとclojure.specの
基本


  • 準標準ライブラリ(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}

テストが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)

基本構文(マクロ)

  • プロパティ: 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)

ジェネレーターに対する主なコンビネーター

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 関数相当)

  • let (return, bind, fmap に対する糖衣構文)

;; 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

見落とされがちな(?)この点が非常に強力😏


冒頭の例をclojure.specによるジェネレーター実装に書き換えると

(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}

clojure.specのspec → test.checkのジェネレーター

;; TODO

⚠️ 利用上の注意点

  • 述語と同名のtest.checkジェネレーターが存在しても完全に同じ実装とは限らない

  • clojure.spec.gen.alpha (sgen)と clojure.test.check.generators (gen)の関係

  • s/and を利用する場合にはジェネレーター実装があらかじめ用意されている述語をベースにする(またはジェネレーターを独自実装)


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


;; TODO


Further Reading

#!/usr/bin/env bash
# pip install mkslides
open http://localhost:8000 \
&& mkslides serve *.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment