Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing asynchronous code in Elixir

Tags:

I want to test a function which is using Task.async

In order to make my test pass, I need to make it sleep for 100ms before assertions, otherwise the test process is killed before the async task is executed.

Is there a better way?

EDITED, adding code samples :

The code I would like to test (roughly) :

def search(params) do   RateLimiter.rate_limit(fn ->     parsed_params = ExTwitter.Parser.parse_request_params(params)     json = ExTwitter.API.Base.request(:get, "1.1/search/tweets.json", parsed_params)     Task.async(fn -> process_search_output(json) end)     new_max_id(json)   end) end 

And the test I already wrote (working only with the call to sleep)

test "processes and store tweets" do   with_mock ExTwitter.API.Base, [request: fn(_,_,_) -> json_fixture end] do     with_mock TwitterRateLimiter, [rate_limit: fn(fun) -> fun.() end] do       TSearch.search([q: "my query"])       :timer.sleep(100)       # assertions        assert called TStore.store("some tweet from my fixtures")       assert called TStore.store("another one")     end   end end 
like image 880
Chris Avatar asked Feb 26 '15 14:02

Chris


2 Answers

Since the question is a bit vague, I will give the general answer here. The usual technique is to monitor the process and wait for the down message. Something like this:

task = Task.async(fn -> "foo" end) ref  = Process.monitor(task.pid) assert_receive {:DOWN, ^ref, :process, _, :normal}, 500 

Some important things:

  • The fifth element of the tuple is the exit reason. I am asserting the Task exit is :normal. Change that accordingly if you are expecting another exit.

  • The second value in assert_receive is the timeout. 500 miliseconds sounds like a reasonable amount given you currently have a 100 ms sleep.

like image 109
José Valim Avatar answered Nov 25 '22 17:11

José Valim


When I cannot use José's approach involving assert_receive, I use a small helper to repeatedly do assertion / sleep, until the assertion pass or finally times out.

Here is the helper module

defmodule TimeHelper do    def wait_until(fun), do: wait_until(500, fun)    def wait_until(0, fun), do: fun.()    def wait_until(timeout, fun) defo     try do       fun.()     rescue       ExUnit.AssertionError ->         :timer.sleep(10)         wait_until(max(0, timeout - 10), fun)     end   end  end 

It can be used like this in former example:

TSearch.search([q: "my query"]) wait_until fn ->   assert called TStore.store("some tweet from my fixtures")   assert called TStore.store("another one") end 
like image 36
Chris Avatar answered Nov 25 '22 17:11

Chris