elixir

How to assert Elixir messages arrive in order


I am building an Elixir module that sends message in certain order. And I wrote unit tests like this

MyModule.send_msgs(self())
assert_received {:event, 1}
assert_received {:event, 2}
assert_received {:event, 3}

The problem is, even if I shuffle the assert_received order, the test still passes.

MyModule.send_msgs(self())
assert_received {:event, 2}
assert_received {:event, 1}
assert_received {:event, 3}

It seems assert_received doesn't care about the order of message arrival. Not just assert_received, as far as I recall, receive can also get messages out of order. So, my question here is, how can you ensure the order of arriving message if they can be received out of order?


Solution

  • There doesn't seem to be an existing function to do this, but it's easy to make your own macro which asserts that the next message in the mailbox matches a specific pattern:

    defmacro assert_next_receive(pattern, timeout \\ 100) do
      quote do
        receive do
          message ->
            assert unquote(pattern) = message
        after unquote(timeout) ->
          raise "timeout" # you might want to raise a better message here
        end
      end
    end
    

    Test:

    defmodule MTest do
      use ExUnit.Case
    
      def send_msgs(pid) do
        send pid, {:event, 1}
        send pid, {:event, 2}
        send pid, {:event, 3}
      end
    
      defmacro assert_next_receive(pattern, timeout \\ 100) do
        quote do
          receive do
            message ->
              assert unquote(pattern) = message
          after unquote(timeout) ->
            raise "timeout" # you might want to raise a better message here
          end
        end
      end
    
      test "should pass" do
        send_msgs(self())
        assert_next_receive {:event, 1}
        assert_next_receive {:event, 2}
        assert_next_receive {:event, 3}
      end
    
      test "should fail" do
        send_msgs(self())
        assert_next_receive {:event, 1}
        assert_next_receive {:event, 3}
        assert_next_receive {:event, 2}
      end
    end
    

    Output:

    .
    
      1) test should fail (MTest)
         test/m_test.exs:29
         match (=) failed
         code:  {:event, 3} = message
         right: {:event, 2}
         stacktrace:
           test/m_test.exs:32: (test)
    
    
    
    Finished in 0.05 seconds
    2 tests, 1 failure
    
    Randomized with seed 351622