如何对Clojure核心异步Go宏进行单元测试?

17

我正在尝试在使用core.async go宏时编写单元测试。以下是一个朴素的测试编写方式,但似乎go块内的代码没有被执行。

(ns app.core-test
  (:require [clojure.test :refer :all]
            [clojure.core.async :as async]))

(deftest test1 []
  (let [chan (async/chan)]
    (async/go
     (is (= (async/<! chan) "Hello")))
    (async/go
     (async/>! chan "Hello"))))

我设法让以下内容运行起来了,但它非常不专业。

(deftest test1 []
  (let [result (async/chan)
        chan (async/chan)]
    (async/go
     (is (= (async/<! chan) "Hello"))
     (async/>! result true))
    (async/go
     (async/>! chan "Hello"))
    (async/alts!! [result (async/timeout 10000)])))

有什么建议可以让我做得更好吗?

3个回答

26

测试是同步执行的,因此如果您转换为异步,则测试运行程序将不会执行。在Clojure中,您需要通过<!!来阻止测试运行程序,在ClojureScript中,您需要返回一个异步测试对象。这是我在所有我的异步CLJC测试中使用的通用帮助函数:

(defn test-async
  "Asynchronous test awaiting ch to produce a value or close."
  [ch]
  #?(:clj
     (<!! ch)
     :cljs
     (async done
       (take! ch (fn [_] (done))))))

您可以使用它进行测试,它与CLJC兼容,并且看起来不那么“hacky”:
(deftest test1
  (let [ch (chan)]
    (go (>! ch "Hello"))
    (test-async
      (go (is (= "Hello" (<! ch)))))))

在测试驱动开发中,我们希望避免阻塞测试运行器,因此断言测试是否解除阻塞是一个很好的做法。此外,在异步编程中,锁定是失败的常见原因,因此针对它进行测试非常合理。

为了实现这一点,我编写了类似于超时函数的辅助函数:

(defn test-within
  "Asserts that ch does not close or produce a value within ms. Returns a
  channel from which the value can be taken."
  [ms ch]
  (go (let [t (timeout ms)
            [v ch] (alts! [ch t])]
        (is (not= ch t)
            (str "Test should have finished within " ms "ms."))
        v)))

你可以使用它来编写测试,例如:
(deftest test1
  (let [ch (chan)]
    (go (>! ch "Hello"))
    (test-async
      (test-within 1000
        (go (is (= "Hello" (<! ch)))))))

4

你的测试即将结束,然后失败了。如果我在其中加入一个延时(sleep)并使它失败,那么这种情况就更可靠地发生:

user> (deftest test1 []
        (async/<!!
         (let [chan (async/chan)]
           (async/go
             (async/go
               (async/<! (async/timeout 1000))
               (is (= (async/<! chan) "WRONG")))
             (async/go
               (async/>! chan "Hello"))))))
#'user/test1
user> (clojure.test/run-tests)

Testing user

Ran 1 tests containing 0 assertions.
0 failures, 0 errors.
{:test 1, :pass 0, :fail 0, :error 0, :type :summary}
user> 
FAIL in (test1) (form-init8563497779572341831.clj:5)
expected: (= (async/<! chan) "WRONG")
  actual: (not (= "Hello" "WRONG"))

在这里,我们可以看到它的报告显示没有失败,然后它打印出了失败消息。我们可以通过明确协调测试结束和该操作完成来解决此问题,就像在核心异步中的大多数解决方案一样,添加一个chan即可。

user> (deftest test1 []
        (async/<!!
         (let [all-done-chan (async/chan)
               chan (async/chan)]
           (async/go
             (async/go
               (async/<! (async/timeout 1000))
               (is (= (async/<! chan) "WRONG"))
               (async/close! all-done-chan ))
             (async/go
               (async/>! chan "Hello"))
             (async/<! all-done-chan)))))
#'user/test1
user> (clojure.test/run-tests)

Testing user

FAIL in (test1) (form-init8563497779572341831.clj:6)
expected: (= (async/<! chan) "WRONG")
  actual: (not (= "Hello" "WRONG"))

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
{:test 1, :pass 0, :fail 1, :error 0, :type :summary}

使用alts的解决方案等同于您的解决方案。我不认为您的解决方案是hackey(指笨拙的)。对于异步代码,始终需要注意何时完成任务,即使您有意决定忽略结果。


0

我正在使用类似于Leon的方法,但没有额外的go块:

(defn <!!?
  "Reads from chan synchronously, waiting for a given maximum of milliseconds.
  If the value does not come in during that period, returns :timed-out. If
  milliseconds is not given, a default of 1000 is used."
  ([chan]
   (<!!? chan 1000))
  ([chan milliseconds]
   (let [timeout (async/timeout milliseconds)
         [value port] (async/alts!! [chan timeout])]
     (if (= chan port)
       value
       :timed-out))))

你可以简单地使用它:

(is (= 42 (<!!? result-chan)))

大多数情况下,我只想从通道中读取值,而不需要任何额外的麻烦。


2
我的解决方案旨在同时在Clojure和Clojurescript中运行,以便编写跨平台单元测试。 - Leon Grapenthin

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