Join the Stack Overflow Community
Stack Overflow is a community of 4.7 million programmers, just like you, helping each other.
Join them; it only takes a minute:
Sign up

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
share|improve this question
1  
Can you show us a minimal failing example that illustrates the specific type of assertion you want to make? – Patrick Oscity Feb 26 '15 at 14:40
    
Example code would help immensely in giving you a good answer. – Onorio Catenacci Feb 26 '15 at 15:49
    
Ok just added code samples – Chris Feb 26 '15 at 17:32
    
Any other comment on my code are OK :) (especially the mocking part) – Chris Feb 26 '15 at 17:40
1  
If you are not going to use the result of the task, don't use Task.async/1, you can use Task.start_link/1 directly. – José Valim Feb 26 '15 at 19:58
up vote 17 down vote accepted

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.

share|improve this answer
    
Thanks you José! Just added some code samples to my question. I guess I need to make my search function to return a tuple containing the Task pid ? (I don't like to change my code only for test purposes :/ ) – Chris Feb 26 '15 at 17:34
4  
Let me argue in favor of that. Your test is a consumer of your code as any other part of your application. Ensuring you are providing a proper result for tests is generally a good indicator other parts of your application will use that code properly too. For example, looking at the tests, I have no idea what the return results are. What happens if I reach the API limit? What happens if I don't? It seems the function is about side-effects and returning a task would indicate: hey, I will finish this later, watch this task if you care about it. – José Valim Feb 26 '15 at 19:57
    
Got it, thanks! – Chris Feb 26 '15 at 21:20

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
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.