【问题标题】:Spec: partially overriding generators in a map spec规范:部分覆盖地图规范中的生成器
【发布时间】:2021-10-20 23:17:34
【问题描述】:

假设我已经定义了一个要从中生成测试数据的规范:

(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]))

在生成测试数据时,我想覆盖 id 的生成方式,以确保它们来自较小的池以鼓励冲突:

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

【问题讨论】:

    标签: clojure clojure.spec test.check


    【解决方案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)))
    

    【讨论】:

    • 这对我有用,但需要注意的是,只有当我的规范基类型没有直接引用另一个规范时。例如,给定: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]
    

    创建的示例 使用my favorite template project


    更新

    不幸的是,上述技术不适用于 Clojure Spec,因为 (s/def ...) 使用 Spec 定义的全局注册表,因此不受 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,而只使用仅在测试环境中本地的不同规范。

      【讨论】:

        猜你喜欢
        • 2020-05-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-03-26
        • 2019-06-21
        • 2011-02-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多