Clojure.Spec在:pre中进行验证时的有意义的错误消息

24

我利用最近几天深入研究了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守卫中获得更好的错误消息吗?

我知道conformexplain*,但那对于这些:pre:post守卫没有帮助。

4个回答

12

在更新的 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}

3
在"pre:"和":post"的“hooks”中能够使用s/assert或s/explain*会很好。 - Anders Eriksson
你可以在前置和后置中使用它们吗? - Alex Miller
当我在 (defn f [x] {:pre [(s/assert string? x)]} (println x)) 中使用 (失败的) s/assert 时,什么也没有发生。但是 s/explain 可以捕获它,所以对我来说没问题。(1.9.0-alpha13) - Anders Eriksson
1
你是否启用了断言?你需要调用https://clojure.github.io/clojure/branch-master/clojure.spec-api.html#clojure.spec/check-asserts,或设置clojure.spec.check-asserts系统属性来启用它们。 - Alex Miller
你是对的。在启用断言的情况下,(s/assert)会起作用。我的问题源于我将(s/assert string? x)(assert string? x)(来自clojure.core)混淆了。后者需要写成(assert (string? x))才能发挥作用。 - Anders Eriksson
显示剩余2条评论

6
我认为这个想法是使用 spec/instrument 验证函数的输入和输出而不是预置和后置条件。
这篇博客文章末尾有一个很好的例子: http://gigasquidsoftware.com/blog/2016/05/29/one-fish-spec-fish/。简单地说,你可以用 `:args` 和 `:ret` 键(因此替代了先决条件和后置条件)定义函数的规范,用 spec/fdef 进行测试,并进行验证。当它不符合规范时,您会得到与使用 explain 相似的输出。
从该链接衍生出的最小示例:
(spec/fdef your-func
    :args even?
    :ret  string?)


(spec/instrument #'your-func)

这相当于给函数设定一个前置条件,即函数必须有一个整数参数,并设定一个后置条件,即函数返回一个字符串。除此之外,你还能获得更有用的错误提示,就像你想要的那样。

更多细节请参阅官方指南:https://clojure.org/guides/spec --- 请查看“对函数进行规格化”章节。


1
听起来合理。会尝试一下。 - DiegoFrings
4
然而,这将不会检查返回值,因此仪器只替换 :pre。这个想法是你使用 test.check 来测试函数是否正确映射输入输出。然后你通过正常测试来仪器化和测试函数的集成。最后,你可以在需要生产保护的地方使用 s/assert,例如 Alex Miller 的答案中提到的那样。 - Didier A.
1
ret值也可以使用orchestra进行检测。https://github.com/jeaye/orchestra - xtreak
我强烈推荐像@xtreak所说的那样使用乐队。如果您在重新加载工作流程中运行repl/refesh,请不要忘记每次重新检测所有函数。将乐队与expound配对,可以获得快速、漂亮的失败信息。 - Conan

2

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

如何让Clojure的:pre和:post报告它们失败的值?

那么你的代码可能会像这样:

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

我对在生产代码中需要测试clojure.test命名空间并不确定。但这似乎是一个可行的选择,可以从:pre保护中获得有意义的消息。 - DiegoFrings
@DiegoFrings clojure.test/is表达式在生产代码中存在的问题似乎有点特殊: 实际运行测试(通过lein test)会破坏计数器。不仅deftest表达式中的is表达式被计入测试通过/失败总数,而且任何由生产代码调用的is也会被计算。否则,人们可以将clojure.test/is视为仅返回布尔值的另一个函数,通过可重写的 clojure.test/report 生成输出。毕竟,人们可以在生产Java代码中使用 Hamcrest matchers,而不仅仅是在JUnit类中。 - David Tonhofer

2

当你不想使用 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)))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接