如何以一对多的方式组合clojure.test.check生成器?

时间:2014-05-15 17:52:36

标签: testing clojure generative-testing

我们假设我有一个生成器users-gen,它生成一个包含1个或更多用户的组。另一个名为user-actions-gen的参数化生成器,它接受一个或多个用户的序列,并生成这些用户可能执行的一系列操作。

(def user-gen 
 ;; generates a user
 ...)

(def users-gen 
 ;; sequences of 1 or more users 
 (gen/such-that not-empty (gen/vector gen/users))

(defn user-actions-gen [users]
  ;; a action performed by some user from the `users argument
  ...)

如果我想为用户生成的单个用户序列生成单个动作,那么它很简单,只需直接生成/绑定用户 - gen到gen-action-gen。

但是,我想从相同的用户序列生成许多操作。我有这个问题,因为我基本上只是想说“这是状态,让任何随机动作进来,让我们将动作应用到状态,让我们确认状态仍然有效;为所有动作执行此操作。 “我有以下代码。

(defspec check-that-state-is-always-valid
  100
  (let [state-atm (atom {})]
    (prop/for-all
     [[actions users]
      (gen/bind users-gen
                (fn [users]
                  (gen/tuple
                   (gen/vector (user-actions-gen users))
                   (gen/return users))))]
     (doseq [action actions
             :let [state (swap! state-atm state-atm-transform-fx action)]]
       (is (state-still-valid? state))))))

这种作品。问题是:

  1. 似乎完全评估剂量而不是停止第一个错误
  2. 看起来有点不对劲。代码遍布整个地方,它的作用并不完全明显。
  3. 似乎user-actions-gen应该是一个用户生成器,而不是用户gen的实现用户价值?这有助于可组合性吗?请注意,我不想将它们放在一起,因为user-gen可能对其他生成器很有用。
  4. 所以,回顾一下。我从一个生成器中获取一个生成的值,并将其作为参数传递给多个生成器。我如何以更有吸引力/更优雅的方式做到这一点?

2 个答案:

答案 0 :(得分:2)

您可能想查看最新版本的test.check0.9.0)。它现在在generator命名空间中包含一个let,这使得组合生成器变得轻而易举:

https://github.com/clojure/test.check/blob/master/src/main/clojure/clojure/test/check/generators.cljc#L1452

这样做的缺点是你仍然无法直接在prop/for-all中执行此操作(显然是由于向后兼容性原因)。

替代方案和我建议使用test.chuck(它由test.check的当前维护者编写)。它有一个for-all,其绑定表单就像generators/let一样工作。这是我发现的最干净的方法,效果非常好。

答案 1 :(得分:1)

我会对您目前正在做的事情进行两项主要更改:

  1. 将您的内联gen/bind拉出一个新的生成器,暂定名为users-and-actions-gen(另请注意,我在结果中交换了usersactions的顺序匹配名称):

    (def users-and-actions-gen
      (gen/bind users-gen
                (fn [users]
                  (gen/tuple
                   (gen/return users)
                   (gen/vector (user-actions-gen users))))))
    
  2. 不要使用原子来测试不需要它的东西。您可以生成一个懒惰的状态序列,并测试它们都具有您正在寻找的属性。这样它可以很好地读取,并具有您正在寻找的短路特性:

    (defspec check-that-state-is-always-valid
      100
      (prop/for-all [[users actions] users-and-actions-gen]
        (is (every? state-still-valid? 
                    (reductions (fn [state action]
                                  (state-atm-transform-fx state action))
                                {} actions)))))
    
  3. 除了这两个变化之外,我不确定你能否真正改善你正在做的事情。我认为你的users-and-actions-gen非常具体。它可以略微概括,但我不确定泛化是否有用(它本质上是一个受限制的bind)。我上面提到的应该解决你的问题(1)和(2),但我不认为(3)确实是一个问题。