Как мне скомпоновать генераторы clojure.test.check по принципу "один ко многим"?

Допустим, у меня есть генератор 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
  ...)

Если я хочу сгенерировать одно действие для одной последовательности пользователей, сгенерированных users-gen, то это просто, просто gen / привязать users-gen непосредственно к user-actions-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 должен брать генератор users-gen, а не реализованную пользовательскую ценность users-gen? Поможет ли это с компоновкой? Обратите внимание, что я не хочу собирать их вместе, так как users-gen, вероятно, полезен для других генераторов.

Итак, подведем итоги. Я беру одно сгенерированное значение из одного генератора и передаю его в качестве аргумента нескольким генераторам. Как мне сделать это более привлекательным / элегантным способом?

2 ответа

Вы можете проверить новейшую версию test.check (0.9.0). Теперь он включает в себя 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. Вытащите свой встроенный gen/bind в новый генератор, предварительно названный users-and-actions-gen (также обратите внимание, что я поменялся местами users а также actions в результате совпадать с именем):

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

Кроме этих двух изменений, я не уверен, что вы действительно можете улучшить то, что вы делаете. Я думаю твой users-and-actions-gen довольно специфично. Это может быть немного обобщено, но я не уверен, что обобщение было бы настолько полезным (это было бы по существу ограниченным bind). То, что я предложил выше, должно решить ваши проблемы (1) и (2), но я не думаю, что (3) действительно проблема.

Другие вопросы по тегам