【问题标题】:Clojure Unit testing : Check if a function was calledClojure 单元测试:检查是否调用了函数
【发布时间】:2013-08-22 03:22:27
【问题描述】:

我正在尝试为我的 clojure 函数编写一些单元测试(我正在使用 clojure.test,但如果需要我可以切换到 midje)。

我有一个函数,如下所示:

(defn GenerateNodes
   [is-sky-blue? hot-outside? name]
   (cond
     (is-sky-blue? name) (generate-sky-nodes)
     (hot-outside?) (generate-hot-nodes)))

当对这个函数进行单元测试时,我想编写如下测试用例:

(deftest when-sky-blue-then-generate-sky-nodes
   (let [is-sky-blue true]
       (GenerateNodes (fn[x] println "sky nodes generated."))
          (is (= true Was-generate-hot-nodes-called?))

如何断言函数 generate-sky-nodes 被调用?或不 ? 我会在 C# 或 java 中使用模拟框架,但我不了解 clojure。

【问题讨论】:

  • 好问题。在我看来,您正在尝试将命令式样式测试应用于声明性代码。您不应该描述 事物是如何工作的,而应该描述 what 它们的作用,因此调用的函数在 IMO 中是一个无关紧要的细节。不过有待函数式编程专家确认(我不是)。
  • @guillaume31,我认为这是模拟和存根之间的区别。存根只是为了提供支持行为的虚假实现,而模拟可以做到这一点,也可以做会计。就我个人而言,我发现模拟是非常糟糕的主意,即使在 OO 世界中也是如此。在功能世界中更是如此。但可能只有我一个人。
  • @ivant Dunno。我猜存根仍然以某种方式描述了如何,尽管您可能无法没有它们来获得性能测试。我个人认为 Mocks 很有用,不是做微记账,而是验证一个对象不会与它的对等点之一说话粗鲁(即外部协议),从而导致在 OO 类型系统中缺乏对这些协议的流畅执行。
  • @guillaume31,我同意在我看来,将函数视为黑盒并测试它返回的内容会更有意义(在函数式世界中)。但在某些情况下,函数所做的一切都是副作用,您希望确保它以适当的方式影响世界。

标签: unit-testing clojure mocking tdd midje


【解决方案1】:

您可以自己编写一个宏来模拟函数并检查函数是否被调用。或者您可以使用expect-call 库。

(defn check-error [a b]
  (when (= a :bad-val)
  (log :error b)))

; This will pass
(deftest check-logging
  (with-expect-call (log [:error _])
  (check-error :bad-val "abc")))

; This will fail
(deftest check-logging-2
  (expect-call (log [:error _])
  (check-error :good-val "abc")))

【讨论】:

  • 只是一个警告:这个库自 2012 年以来就没有更新过,并且似乎与 Clojure 1.9 不兼容。
【解决方案2】:

您已经拥有的功能离工作版本不远了。我改变了一些东西,使其对 Clojure 更加地道。

以下假设 generate-sky-nodes 和 generate-hot-nodes 每个都返回一些值(除了它们具有的任何副作用之外,还可以这样做),即:

(defn generate-sky-nodes
   []
   (doseq [i (range 10)] (do-make-sky-node i))
   :sky-nodes)

然后,您的 generate-nodes 调整如下:

(defn generate-nodes
  [sky-blue? hot-outside? name]
  (cond
   (sky-blue? name) (generate-sky-nodes)
   (hot-outside?) (generate-hot-nodes)))

最后是测试的功能版本:

(deftest when-sky-blue-then-generate-sky-nodes
  (let [truthy (constantly true)
        falsey (constantly false)
        name nil]
  (is (= (generate-nodes truthy falsey name)
         :sky-nodes))
  (is (= (generate-nodes truthy truthy name)
         :sky-nodes))
  (is (not (= (generate-nodes falsey falsey name)
              :sky-nodes)))
  (is (not (= (generate-nodes falsey truthy name)
              :sky-nodes)))))

一般的想法是你不测试它做了什么,你测试它返回什么。然后你安排你的代码,以便(尽可能)关于函数调用的所有事情都是它返回的内容。

另一个建议是通过使用generate-sky-nodesgenerate-hot-nodes 返回要执行的副作用,以尽量减少发生副作用的位置:

(defn generate-sky-nodes
   []
   (fn []
    (doseq [i (range 10)] (do-make-sky-node i))
    :sky-nodes))

您对generate-nodes 的调用如下所示:

(apply (generate-nodes blue-test hot-test name) [])

或者更简洁(如果你对 Clojure 不太熟悉的话,虽然这很奇怪):

((generate-nodes blue-test hot-test name))

(在上面的测试代码中做了必要的修改,测试也可以在这个版本上工作)

【讨论】:

  • 对于还不熟悉函数范式的人来说,这是一个很好的例子。 +1
  • 好帖子!这种模式是否有一个名称,副作用函数返回一个有助于测试的“令牌”?
【解决方案3】:

使用Midjecheckables

(unfinished is-sky-blue? hot-outside?)
(facts "about GenerateNodes"
  (fact "when the sky is blue then sky nodes are generated"
    (GenerateNodes is-sky-blue? hot-outside? ..name..) => ..sky-nodes..
    (provided
      (is-sky-blue? ..name..) => true
      (generate-sky-nodes)    => ..sky-nodes..
      (generate-hot-nodes)    => irrelevant :times 0)))

【讨论】:

    【解决方案4】:

    您可以使用mock-clj

    (require ['mock-clj.core :as 'm])
    
    (deftest when-sky-blue-then-generate-sky-nodes
      (m/with-mock [is-sky-blue? true
                    generate-sky-nodes nil]
        (GenerateNodes (fn[x] println "sky nodes generated.") ... ...)
        (is (m/called? generate-sky-nodes))))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-13
      • 2021-12-30
      相关资源
      最近更新 更多