Assert condition with timeout

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

:timer.sleep(100)
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."
      end

      def test_assertion(t) do
        try do
          unquote(assertion)
          :ok
        rescue
          _ ->
            :timer.sleep(1)
            test_assertion(t - 1)
        end
      end
    end

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

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.

2 Likes

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.

https://github.com/elixir-lang/elixir/blob/v1.5.3/lib/logger/lib/logger.ex#L699

2 Likes

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)
  end

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

end

Initialize the test message registry in the test_helper.exs as

TestMessage.Registry.init()

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))
    end
  end

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

end

Thanks again. This has been really helpful.

Just another idea, following @josevalim rationally and with no external dependencies. Code isn’t readable since Erlang trace API is complicated. But if you abstract the complication away in test helper functions, it can become manageable…

pattern = {Module, :function, 0} # it can be any {mod, fun, arity}
:erlang.trace_pattern(pattern, [{:_, [], [{:return_trace}]}])
:erlang.trace(:all, true, [:call])

# do async work that ends up calling Module.function()

assert_receive {:trace, _pid, :return_from, ^pattern, {:noreply, _state}} # block until Module.function is called

# write the rest of your assertions here, the ones that need `Module.function` to be returned...