Assert condition with timeout

Tags: #<Tag:0x00007f11414f0048>


In my test suite I occasionally find myself needing to work with timeouts if some processing is done asynchronously. In this case I often resort to

assert condition

Is there a way to instead specify an assertion that takes a condition and a timeout with the following logic: If the condition holds before the timeout is up then the test passes. Otherwise it fails. If that assertion would not simply implement the above but check regularly, say every ms, then this would allow for

  • a more robust test suite as the timeout can be put to a much larger value than the above sleep workaround
  • for a much faster test suite, since the full sleep is usally superfluous.

Implementing this functionality with a macro should be (I guess) straight forward. If so, are there by any chance already any libraries that offer this type of assertion or if not, is the above an anti-pattern that should be avoided?


It is definitely an anti-pattern as you will always 100ms even if the condition you are asserting on would be true after 1ms.

Ideally you want to assert_receive and assert on messages or perform a blocking call. For example, if you are talking to a server that performs async actions, you could have a sync call that simply returns ok. That in itself will guarantee all async messages have been processed.


Agreed, the sleep workaround is an anti pattern. I was thinking more in the lines of the following

defmacro await_assert(assertion, timeout) do
  tmp_module_name = "AwaitedAssertion" <> String.replace(to_string(:rand.uniform()), ".", "")
  quote do
    defmodule :"#{unquote(tmp_module_name)}" do
      def test_assertion(t) when t <= 0 do
        assert false, "Operation took too long."

      def test_assertion(t) do
        try do
          _ ->
            test_assertion(t - 1)

    apply(:"#{unquote(tmp_module_name)}", :test_assertion, [unquote(timeout)])

In that way I would evaluate the condition every millisecond until a timeout is reached after which I fail the assertion. Would you still call this an anti-pattern? An example for a use case would be an asynchronous action that triggers the insertion of a record being inserted into an ets table which I would like to verify.


assert_receive/3 has already been pointed out to you - have a look at it and it’s friends.

  • “Busy wait” is the anti-pattern - it doesn’t matter if the wait is 100 or 1 ms.
  • In an efficient system events worth observing need to be broadcast - not “watched”. Interested parties should be able to register (and unregister) their “interest” so that they can be notified when the event occurs. That notification (event message) can then be used with assert_receive/3.

The fact that you need to structure a test in a “busy wait” manner may suggest that you have a design issue.

What is that ETS table used for? Typically they exist to support some sort of request served by the process. If the same process sends the async insert followed by a synchronous request then the request should be served after the insert has been processed - so the result of the request should be consistent with the insert being successful.


I still have to disagree here. assert_receive/3 is great for many use cases but not suitable for everything. Take as example a request that triggers the insertion of data into a cache. I want to assert now that after the request, data eventually is served from the cache. In this case I would not want to add a broadcast mechanism to the cache that informs interested parties about insertions and invalidations a) because it is expensive and unnecessary (except for testing) and b) because that would be a very different test.


This is no different than logging. Write your ETS access functions in such a way that for testing they compile to send a digest message to a named process for testing. When compiling for production that code can then be safely omitted.


That is a nice idea. Thanks. Macros still amaze me. Here is a little code I’ve been playing around with to implement the suggestion:

defmodule TestMessage.Registry do
  @moduledoc false

  @registry TestMessage.Registry
  @default_key :test_message

  def name, do: @registry
  def default_key, do: @default_key

  def init do
    {:ok, _} = Registry.start_link(keys: :duplicate, name: @registry, partitions: System.schedulers_online)

  def subscribe(key \\ @default_key) do
    {:ok, _} = Registry.register(@registry, key, [])


Initialize the test message registry in the test_helper.exs as


And register for test messages via the subscribe function within test cases. To send test messages use the following module that implements @peerreynders sugestion of checking that the code is compiled for testing

defmodule TestMessage.Sender do
  @moduledoc false

  alias TestMessage.Sender
  alias TestMessage.Registry, as: TestMessageRegistry

  defmacro send_test_message(message) do
    quote do
      Sender.send_test_message(TestMessageRegistry.default_key(), unquote(message))

  defmacro send_test_message(key, message) do
    if Mix.env == :test do
      quote bind_quoted: [key: key, message: message] do
        Registry.dispatch(, key, fn entries ->
          for {pid, _} <- entries, do: send(pid, message)


Thanks again. This has been really helpful.