Unit test an async/spawn function

def async_query(query_def) do
  spawn(fn -> IO.puts(run_query(query_def)) end)

def run_query(query_def) do
  "#{query_def} result"

Normally, we could use ExUnit.CaptureIO.capture_io – but this is an async/spawn.

How do we unit test functions like this? (or show should they be written so they can be unit tested.)

1 Like

captureIO should still work, the problem is that lambda in captureIO needs to wait, at least 50ms.

You should also almost never be using spawn, use Task functions instead, and if this is truly an async process, you should use Task.async. If you do that, then your captureIO can wrap the Task.await; and you don’t have to rely on time coordination between the code and your test.

(disclaimer, self-promotion: THE PROCESS - part 3a (The Conundrum with Concurrency) - YouTube)


For posterity, here’s the link to ExUnit.CaptureIO.capture_io function’s docs.

I am not attributing ill intention to you and I should have been clearer in my original post, but this advice is useless.

I am currently working through Chapter 5. Concurrency primitives · Elixir in Action (which uses spawn). I want to ExUnit test this code as is.

The way that book is structured is supposed to build up on top of primitives. You’re not really supposed to be able to unit test stuff that uses spawn by itself, because spawn is a primitive. Testing will have you run up against limitations of concurrent systems, which you have discovered. Task module is created by Jose in part, to build on on top of the spawn primitives and make testing sane by overcoming these limitations of concurrency.

So, if you are trying to unit test the spawn example from sasa’s book as is, the “way to make it work” Is to rebuild the niceties of Task, which could be a good exercise in and of itself.


Thanks, this makes a lot of sense. What I ended up doing (which unfortunately breaks my own condition for “test code as is”) is to change the IO.puts into a send, and then to do an assert_received in the unit test.