Learn how to test futures, promises, and core.async code with timeouts, deterministic boundaries, and invariant-focused assertions instead of sleep-based flakiness.
Asynchronous test: A test that must observe work completing later or elsewhere without depending on lucky timing.
The biggest mistake in async testing is treating time as the thing under test. It usually is not. What you care about is a contract: a value appears, a channel closes, a timeout occurs, or an invariant still holds after concurrent work finishes.
That is why good async tests are built around explicit completion boundaries and bounded waiting. Thread/sleep is sometimes useful for forcing a condition, but it is a poor default assertion strategy.
These rules catch most flakiness:
The point is to make completion explicit.
core.async CodeThe cleanest pattern is to race the expected result against a timeout:
1(ns async-test.core
2 (:require [clojure.core.async :as async]
3 [clojure.test :refer [deftest is]]))
4
5(defn async-double [x]
6 (let [out (async/chan 1)]
7 (async/go
8 (async/>! out (* 2 x))
9 (async/close! out))
10 out))
11
12(deftest async-double-test
13 (let [result-ch (async-double 5)
14 timeout-ch (async/timeout 200)
15 [value source] (async/alts!! [result-ch timeout-ch])]
16 (is (= result-ch source))
17 (is (= 10 value))))
This pattern says exactly what the test needs: the result should arrive before the timeout.
Futures and promises already expose completion boundaries, so use them directly with bounded waits:
1(deftest future-test
2 (let [f (future (* 3 7))]
3 (is (= 21 (deref f 200 ::timeout)))))
4
5(deftest promise-test
6 (let [p (promise)]
7 (future (deliver p :done))
8 (is (= :done (deref p 200 ::timeout)))))
The timeout fallback sentinel is valuable because it distinguishes “wrong value” from “never completed.”
This is weak:
1(Thread/sleep 100)
2(is (= :done @state))
It is weak because:
A better design exposes a promise, channel, or other observable completion signal and waits on that directly.
Most asynchronous code should be tested through behavioral contracts:
That is much more stable than testing which thread ran first.
If you cannot explain how a test knows the async work is finished, the test is probably still too implicit. Make completion observable, race it against a timeout, and assert on the outcome that actually matters.