arnomi

arnomi

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?

Showing Posts 1 to 7

josevalim

josevalim

Creator of Elixir

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.

arnomi

arnomi OP

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.

peerreynders

peerreynders

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.

arnomi

arnomi OP

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.

peerreynders

peerreynders

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.

elixir/lib/logger/lib/logger.ex at v1.5.3 · elixir-lang/elixir · GitHub

arnomi

arnomi OP

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.

alexandre

alexandre

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...
— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
water
I’m aware of 2 such projects, are there others? git: GitHub - team-alembic/realworld: A fullstack Phoenix LiveView application with back...
New
katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
bradley
I really like the adapter patterns that ecto, nebulex, waffle, etc. use and would love find something similar for a key management servic...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
Alvinkariuki
How Can I Optimise Compile Time Dependencies I have been building an elixir application for about 2 years now. Many modules and files ha...
New

Other Trending Topics Top

KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews