Spec

Начиная с версии 1.9 clojure поставляется с библиотекой clojure.spec. Она добавляет возможность создания спецификаций данных и функций. Благодаря спецификациям можно

  • валидировать данные
  • генерировать тестовые данные
  • разбирать данные на составные части (destructuring)
  • проверять входные и выходные параметры функций
  • автоматически тестировать функции (generative tests)

Ознакомьтесь с официальными материалами:

Поэкспериментируйте с библиотекой в тестовом проекте.

Обратите внимание на комментарии ниже.

Комментарии

Спецификации напоминают статическую типизацию, только проверки выполняются в рантайме. Однако, есть экспериментальный проект spectrum запускающий проверки спецификаций в compile time.


При использовании st/instrument проверяются только аргументы функции, но не :ret и :fn. Возможно, это поведение изменится, а пока можно воспользоваться библиотекой orchestra, которая позиционируется как замена clojure.spec.test.alpha.


Нужно быть внимательным при создании спецификаций функций высшего порядка, т.к. instrument будет проверять соответствие спецификации принимаемой или возвращаемой функции. Это допустимо для чистых функций, но неприемлемо для функций с побочным эффектом:

(s/fdef f
   :args (s/cat :g (s/fspec
                     :args (s/cat :x int?)
                     :ret int?)))

(defn f [g]
  (g 42))

(require '[clojure.spec.test.alpha :as st])
(st/instrument `f)

(f str)
;;=> ExceptionInfo Call to #'user/f did not conform to spec:
;;=> In: [0] val: "0" fails at: [:args :g :ret] predicate: int?

(f inc)
;;=> 43

(f (fn [x] (prn x) x))
-1
0
-1
0
-4
-1
-2
-11
-4
0
5
81
-3
196
12
-1853
83
1399
-3
-11
-57026
42
;;=> 42

Выход - просто проверять, что аргумент любая функция:

(s/fdef f :args (s/cat :g fn?))

Instrument не работает для протоколов. Используйте обертки:

(defprotocol P
  (-foo [x y z]))

(s/fdef foo ...)

(defn foo [x y z]
  (-foo x y z))

Хочется использовать spec для валидации форм, но сгенерировать понятные пользователю сообщения об ошибках из структуры explain-data - нетривиальная задача. В этом поможет библиотека phrase.


Генератор не всемогущ и использует перебор:

(s/def ::login (s/and string? #(re-matches #"\w{3,255}" %)))
(-> ::login s/gen sgen/generate) ;;=> "wbW8"

;; UTF symbols
(s/def ::smile (s/and string? #(re-matches #"[☺☹]" %)))
(-> ::smile s/gen sgen/generate)
;;=> ExceptionInfo Couldn't satisfy such-that predicate after 100 tries.

Существуют библиотеки генераторов, которые, например, могут по регулярному выражению сгенерировать требуемую строку: test.chuck, strgen.