【问题标题】:Meaningful error message for Clojure.Spec validation in :preClojure.Spec 验证的有意义的错误消息:pre
【发布时间】:2016-10-19 12:37:52
【问题描述】:

我在最后几天深入研究了 Clojure 和 ClojureScript 中的 clojure.spec。

到目前为止,我发现它最有用的是,在依赖特定格式数据的公共函数中使用规范作为 :pre 和 :post 的守卫。

(defn person-name [person]
  {:pre [(s/valid? ::person person)]
   :post [(s/valid? string? %)]}
  (str (::first-name person) " " (::last-name person)))

这种方法的问题是,我收到了一个java.lang.AssertionError: Assert failed: (s/valid? ::person person),但没有任何关于究竟什么不符合规范的信息。

有人知道如何在:pre 或:post 守卫中获得更好的错误消息?

我知道conform 和explain*,但这对那些:pre 或:post 守卫没有帮助。

【问题讨论】:

    标签: clojure clojure.spec


    【解决方案1】:

    这在您不想使用s/assert 或无法启用s/check-assserts 时很有用。改进 MicSokoli 的回答:

    :pre 只关心返回的值都是真实的,所以我们可以将返回值"Success!\n" 转换为true(为了严格)和throw 的解释和输入数据出错,以防万一输出不成功。

    (defn validate [spec input]
        (let [explanation (s/explain-str spec input)]
            (if (= explanation "Success!\n") 
                true
                (throw (ex-info explanation {:input input}))))
    

    这个变种可能是这个,但它会运行规范两次:

    (defn validate [spec input]
        (if (s/valid? spec input) 
            true
            (throw (ex-info (s/explain spec input) {:input input}))))
    

    用法:

    (defn person-name [person]
      {:pre [(validate ::person person)]}
      (str (::first-name person) " " (::last-name person)))
    

    【讨论】:

      【解决方案2】:

      在较新的 alpha 版本中,现在有 s/assert 可用于断言输入或返回值与规范匹配。如果有效,则返回原始值。如果无效,解释结果将引发断言错误。断言可以打开或关闭,甚至可以选择从编译代码中完全省略以产生 0 生产影响。

      (s/def ::first-name string?)
      (s/def ::last-name string?)
      (s/def ::person (s/keys :req [::first-name ::last-name]))
      (defn person-name [person]
        (s/assert ::person person)
        (s/assert string? (str (::first-name person) " " (::last-name person))))
      
      (s/check-asserts true)
      
      (person-name 10)
      => CompilerException clojure.lang.ExceptionInfo: Spec assertion failed
      val: 10 fails predicate: map?
      :clojure.spec/failure  :assertion-failed
       #:clojure.spec{:problems [{:path [], :pred map?, :val 10, :via [], :in []}], :failure :assertion-failed}
      

      【讨论】:

      • 如果能够在 pre: 和 :post "hooks" 中使用 s/assert 或 s/explain* 那就太好了...
      • 你可以在前后使用它们吗?
      • 当我在(defn f [x] {:pre [(s/assert string? x)]} (println x)) 中有这个(失败的)s/assert 时没有任何反应,但是 s/explain 确实捕获了它,所以这对我来说很好。 (1.9.0-alpha13)
      • 您是否启用了断言?您需要调用 clojure.github.io/clojure/branch-master/… 或设置 clojure.spec.check-asserts 系统属性来启用它们。
      • 你是对的。 (s/assert) 在启用断言的情况下工作。我的问题源于我将(s/assert string? x) 与(assert string? x) (来自clojure.core)混合在一起。后者需要写(assert (string? x)) 才能生效。
      【解决方案3】:

      在不考虑是否应该使用前置条件和后置条件来验证函数参数的情况下,有一种方法可以通过使用clojure.test/is 包装谓词来从前置条件和后置条件中打印出更清晰的消息,如下面的答案所示:

      How can I get Clojure :pre & :post to report their failing value?

      那么您的代码可能如下所示:

      (ns pre-post-messages.core
        (:require [clojure.spec :as s]
                  [clojure.test :as t]))
      
      (defn person-name [person]
        {:pre [(t/is (s/valid? ::person person))]
         :post [(t/is (s/valid? string? %))]}
        (str (::first-name person) " " (::last-name person)))
      
      (def try-1
        {:first-name "Anna Vissi"})
      
      (def try-2
        {::first-name "Anna"
         ::last-name "Vissi"
         ::email "Anna@Vissi.com"})
      
      (s/def ::person (s/keys :req [::first-name ::last-name ::email]))
      

      评估

      pre-post-messages.core> (person-name  try-2)
      

      会产生

      "Anna Vissi"
      

      和评估

      pre-post-messages.core> (person-name  try-1)
      

      会产生

      FAIL in () (core.clj:6)
      
      expected: (s/valid? :pre-post-messages.core/person person)
      
        actual: (not (s/valid? :pre-post-messages.core/person {:first-name "Anna Vissi"}))
      
      AssertionError Assert failed: (t/is (s/valid? :pre-post-messages.core/person person))  pre-post-messages.core/person-name (core.clj:5)
      

      【讨论】:

      • 我不确定在生产代码中是否需要 test clojure.test 命名空间。但这似乎是一个可行的选择,可以从:pre 警卫那里获得有意义的信息。
      • @DiegoFrings 生产代码中clojure.test/is 表达式的问题似乎有点特殊:实际运行测试(通过lein test)会弄乱计数器。不仅deftest 表达式中的is 表达式计入测试通过/失败总数,而且生产代码调用的任何is 也被计入。否则,可以将clojure.test/is 视为另一个返回布尔值的函数,生成通过可覆盖的clojure.test/report 写入的输出。毕竟,一个人可以使用例如生产 Java 代码中的 Hamcrest 匹配器也是如此,不仅在 JUnit 类中。
      【解决方案4】:

      我认为这个想法是您使用spec/instrument 来验证函数输入和输出,而不是前置条件和后置条件。

      在这篇博文的底部有一个很好的例子:http://gigasquidsoftware.com/blog/2016/05/29/one-fish-spec-fish/。快速总结:您可以使用 spec/fdef 为函数定义规范,包括使用 :args 和 :ret 键(因此替换前置条件和后置条件)的输入和返回值,检测它,您将获得类似于在不符合规范时使用explain。

      从该链接派生的最小示例:

      (spec/fdef your-func
          :args even?
          :ret  string?)
      
      
      (spec/instrument #'your-func)
      

      这相当于设置函数具有整数参数的前提条件和返回字符串的后置条件。除了你得到更多有用的错误,就像你正在寻找的那样。

      官方指南中的更多详细信息:https://clojure.org/guides/spec ---请参阅“规范功能”标题下。

      【讨论】:

      • 听起来很合理。会试试的。
      • 这不会检查返回值,所以instrument只替换:pre。这个想法是你使用 test.check 来测试你的函数是否正确地将输入映射到输出。然后,您通过常规测试在集成中检测和测试您的功能。最后,您可以在任何需要生产保护的地方使用 s/assert,例如 Alex Miller 的回答中提到的。
      • :ret 值也可以使用管弦乐队进行检测。 github.com/jeaye/orchestra
      • 我强烈建议使用@xtreak 所说的管弦乐队。如果您将repl/refesh 作为reloaded 工作流程的一部分运行,请不要忘记每次都重新设置所有fns。将管弦乐队与 expound 配对,以实现快速、漂亮的失败。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-04-08
      • 2017-01-15
      • 2016-04-18
      • 2016-12-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多