Как выполнить unit тест clojure.core.async Перейти к макросам?
Я пытаюсь написать модульные тесты при использовании макросов 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)])))
Любые предложения, как я могу это сделать правильно?
Ответы
Ответ 1
ваш тест заканчивается, а затем сбой. Это происходит более надежно, если я засыпаю, а затем сделаю это неудачным:
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"))
здесь мы видим, что он сообщает, что ничего не получается, затем печатает сообщение об ошибке. Мы можем исправить это, явно координируя конец теста и завершая действие, как и большинство решений в core.async, добавляя еще один 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}
Это эквивалентно вашему решению с использованием альтов. Я не думаю, что ваше решение - хаки. С асинхронным кодом всегда требуется обратить внимание на то, когда все закончится, даже если вы решительно решили проигнорировать результат.
Ответ 2
Тесты выполняются синхронно, поэтому, если вы перейдете на асинхронный тест, бегун не будет. В Clojure вам нужно заблокировать тестового бегуна через <!!
, в ClojureScript вы должны вернуть тестовый объект async. Это универсальная вспомогательная функция, которую я использую во всех своих тестах асинхронного 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 совместим и выглядит менее "взломанным":
(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)))))))
Ответ 3
Я использую подход, похожий на Leon, но без дополнительных блоков:
(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)))
Большую часть времени я просто хочу прочитать значение с канала без лишних хлопот.