A Better Solution for Waiting for Async Tasks in Tests

A row of white chairs in front of a gray wall beneath a gray clock with black and red hands

Whether you need end-to-end product development support or the engineering know-how for a targeted project, we can help. Book a free consult to learn more.

When you’re writing tests for code that involves async tasks, sometimes you need to make sure that your tests wait for the async tasks to complete before continuing with the test or making assertions. Or you might just be tired of seeing a bunch of Ecto sandbox error logs when your test process terminates and your async tasks were using the database.

The quick and dirty solution is to throw in some sort of sleep function to wait for a certain number of milliseconds before continuing with the test. However, this is imprecise and requires you to draw a balance between fragility and slowness.

A better solution is to use assert_receive and here is a test helper for Supervised Tasks that can help with that:

def flush_task_supervisor do
  pids = Task.Supervisor.children(MyApp.TaskSupervisor)

  for pid <- pids do
    ref = Process.monitor(pid)
    assert_receive {:DOWN, ^ref, _, _, _}
  end
end

Then in your test, you can call flush_task_supervisor/0 to wait for the tasks to complete:

test "does something async" do
  MyApp.do_something_async()

  flush_task_supervisor()

  assert MyApp.check_something()
end

I got this tip (or something like it) several years ago, and I’m pretty sure it was José Valim who suggested it, so shout out to him for always being helpful to the Elixir community.

I end up using this in almost every project that has tasks, and maybe you should too!

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box