Спецификация: частично переопределяющие генераторы в спецификации карты

clojure #clojure.spec #test.check

#clojure #clojure.spec #тест.проверка

Вопрос:

Предполагая, что я уже определил спецификацию, из которой я хотел бы сгенерировать тестовые данные:

 (s/def :customer/id uuid?)
(s/def :customer/given-name string?)
(s/def :customer/surname string?)
(s/def :customer/age pos?)
(s/def ::customer
  (s/keys
    :req-un [:customer/id
             :customer/given-name
             :customer/surname
             :customer/age]))
 

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

 (defn customer-generator
  [id-count]
  (gen/let [id-pool (gen/not-empty (gen/vector (s/gen :customer/id) id-count))]
    (assoc (s/gen ::customer) :id (gen/element id-pool))))
 

Есть ли способ упростить это, переопределив :customer/id генератор в моем тестовом коде, а затем просто используя (s/gen ::customer) ? Итак, что-то вроде следующего:

 (with-generators [:customer/id (gen/not-empty (gen/vector (s/gen :customer/id) id-count)))]
  (s/gen ::customer))
 

Ответ №1:

Официально вы можете переопределить генераторы для спецификаций, передав переопределенную карту s/gen (подробнее см. Строку документа):

 (s/def :customer/id uuid?)
(s/def :customer/given-name string?)
(s/def :customer/surname string?)
(s/def :customer/age nat-int?)
(s/def ::customer
  (s/keys
    :req-un [:customer/id
             :customer/given-name
             :customer/surname
             :customer/age]))

(def fixed-customer-id (java.util.UUID/randomUUID))
fixed-customer-id
;=> #uuid "c73ff5ea-8702-4066-a31d-bc4cc7015811"
(gen/generate (s/gen ::customer {:customer/id #(s/gen #{fixed-customer-id})}))
;=> {:id #uuid "c73ff5ea-8702-4066-a31d-bc4cc7015811",
;    :given-name "1042IKQhd",
;    :surname "Uw0AzJzj",
;    :age 104}
 

Кроме того, для таких вещей есть библиотека с именем genman, которую я разработал ранее 🙂
Используя его, вы также можете записать как:

 (require '[genman.core :as genman :refer [defgenerator]])

(def fixed-customer-id (java.util.UUID/randomUUID))

(genman/with-gen-group :test
  (defgenerator :customer/id
    (s/gen #{fixed-customer-id})))

(genman/with-gen-group :test
  (gen/generate (genman/gen ::customer)))
 

Комментарии:

1. Это сработало для меня, с оговоркой, что только тогда, когда мои базовые типы спецификаций напрямую не ссылались на другую спецификацию. Например, дано: clj (s/def :data/uuid uuid?) (s/def :customer/id :data/uuid) попытка переопределить :customer/id генератор тогда не работает, но переопределение :data/uuid работает. Обходной путь, который я нашел, был таким: clj (s/def :customer/id (s/and :data/uuid)) который кажется немного запутанным.

Ответ №2:

Спецификация Clojure использует test.check внутренне для генерации выборочных значений. Вот как test.check можно переопределить. Всякий раз, когда вы пытаетесь написать модульные тесты с «поддельной» функцией, with-redefs — ваш друг:

 (ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [clojure.test.check.generators :as gen]
    ))

(def  id-gen gen/uuid)
(dotest
  (newline)
  (spyx-pretty (take 3 (gen/sample-seq id-gen)))

  (newline)
  (with-redefs [id-gen (gen/choose 1 5)]
    (spyx-pretty (take 33 (gen/sample-seq id-gen))))
  (newline)
  )
 

с результатом:

 -----------------------------------
   Clojure 1.10.3    Java 15.0.2
-----------------------------------

Testing tst.demo.core

(take 3 (gen/sample-seq id-gen)) => 
[#uuid "cbfea340-1346-429f-ba68-181e657acba5"
 #uuid "7c119cf7-0842-4dd0-a23d-f95b6a68f808"
 #uuid "ca35cb86-1385-46ad-8fc2-e05cf7a1220a"]

(take 33 (gen/sample-seq id-gen)) => 
[5 4 3 3 2 2 3 1 2 1 4 1 2 2 4 3 5 2 3 5 3 2 3 2 3 5 5 5 5 1 3 2 2]
 

Пример, созданный
с использованием моего любимого проекта шаблона.


Обновить

К сожалению, описанный выше метод не работает для спецификации Clojure, поскольку (s/def ...) использует глобальный реестр определений спецификаций и, следовательно, невосприимчив к with-redefs . Однако мы можем преодолеть это определение, просто переопределив нужную спецификацию в пространстве имен модульного теста, например:

 (ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [clojure.spec.alpha :as s]
    [clojure.spec.gen.alpha :as gen]
  ))

(s/def :app/id (s/int-in 9 99))
(s/def :app/name string?)
(s/def :app/cust (s/keys :req-un [:app/id :app/name]))

(dotest
  (newline)
  (spyx-pretty (gen/sample (s/gen :app/cust)))

  (newline)
  (s/def :app/id (s/int-in 2 5)) ; overwrite the definition of :app/id for testing
  (spyx-pretty (gen/sample (s/gen :app/cust)))

  (newline))
 

с результатом

 -----------------------------------
   Clojure 1.10.3    Java 15.0.2
-----------------------------------

Testing tst.demo.core

(gen/sample (s/gen :app/cust)) => 
[{:id 10, :name ""}
 {:id 9, :name "n"}
 {:id 10, :name "fh"}
 {:id 9, :name "aI"}
 {:id 11, :name "8v5F"}
 {:id 10, :name ""}
 {:id 10, :name "7"}
 {:id 10, :name "3m6Wi"}
 {:id 13, :name "OG2Qzfqe"}
 {:id 10, :name ""}]

(gen/sample (s/gen :app/cust)) => 
[{:id 3, :name ""}
 {:id 3, :name ""}
 {:id 2, :name "5e"}
 {:id 3, :name ""}
 {:id 2, :name "y01C"}
 {:id 3, :name "l2"}
 {:id 3, :name "c"}
 {:id 3, :name "pF"}
 {:id 4, :name "0yrxyJ7l"}
 {:id 4, :name "40"}]
 

Итак, это немного некрасиво, но переопределение :app/id делает свое дело, и оно вступает в силу только во время модульных тестовых запусков, оставляя основное приложение незатронутым.

Ответ №3:

 user> (def ^:dynamic *idgen* (s/gen uuid?))
#'user/*idgen*
user> (s/def :customer/id (s/with-gen uuid? (fn [] @#'*idgen*)))
:customer/id
user> (s/def :customer/age pos-int?)
:customer/age
user> (s/def ::customer (s/keys :req-un [:customer/id :customer/age]))
:user/customer
user> (gen/sample (s/gen ::customer))
({:id #uuid "d18896f1-6199-42bf-9be3-3d0652583902", :age 1}
 {:id #uuid "b6209798-4ffa-4e20-9a76-b3a799a31ec6", :age 2}
 {:id #uuid "6f9c6400-8d79-417c-bc62-6b4557f7d162", :age 1}
 {:id #uuid "47b71396-1b5f-4cf4-bd80-edf4792300c8", :age 2}
 {:id #uuid "808692b9-0698-4fb8-a0c5-3918e42e8f37", :age 2}
 {:id #uuid "ba663f0a-7c99-4967-a2df-3ec6cb04f514", :age 1}
 {:id #uuid "8521b611-c38c-4ea9-ae84-35c8a2d2ff2f", :age 4}
 {:id #uuid "c559d48d-4c50-438f-846c-780cdcdf39d5", :age 3}
 {:id #uuid "03c2c114-03a0-4709-b9dc-6d326a17b69d", :age 40}
 {:id #uuid "14715a50-81c5-48e4-bffe-e194631bb64b", :age 4})
user> (binding [*idgen* (let [idpool (gen/sample (s/gen :customer/id) 5)] (gen/elements idpool))] (gen/sample (s/gen ::customer)))
({:id #uuid "3e64131d-e7ad-4450-993d-fa651339df1c", :age 2}
 {:id #uuid "575b2bef-956d-4c42-bdfa-982c7756a33c", :age 1}
 {:id #uuid "575b2bef-956d-4c42-bdfa-982c7756a33c", :age 1}
 {:id #uuid "3e64131d-e7ad-4450-993d-fa651339df1c", :age 1}
 {:id #uuid "1a2eafed-8242-4229-b432-99edb361569d", :age 3}
 {:id #uuid "1a2eafed-8242-4229-b432-99edb361569d", :age 1}
 {:id #uuid "05bd521a-26f9-46e0-8b26-f798e0bf0452", :age 3}
 {:id #uuid "575b2bef-956d-4c42-bdfa-982c7756a33c", :age 19}
 {:id #uuid "31b80714-7ae0-40a0-b932-f7b5f078f2ad", :age 2}
 {:id #uuid "05bd521a-26f9-46e0-8b26-f798e0bf0452", :age 5})
user>  
 

Немного неуклюже, чем вы хотели, но, возможно, этого достаточно.

Вероятно, вам лучше использовать binding , а не with-redefs поскольку binding изменяет локальные привязки потока, тогда with-redefs как изменяет корневую привязку.

Поскольку это предназначено для генерации неверных тестовых данных, я бы предпочел избегать использования динамических переменных и binding вообще и просто использовать другую спецификацию, которая является локальной только для тестовой среды.