Very strange: handle_info not called unless I inspect pid messages

This is a very strange behavior I am seeing and I can’t to spot why it happens.
I am using Phoenix.PubSub to send messages to instances of a GenServer, something like this:

def start_link(%{source: source, from: _} = opts) do
    GenServer.start_link(__MODULE__, opts, name: :"for_#{source}")
end

@impl true
def init(%{source: source, from: _} = state) do
    PubSub.subscribe(:mypubsub, source, [])

    {:ok, state}
end

@impl true
def handle_info(message, %{from: from} = state) do
   IO.puts("HANDLE INFO")
   # side effects here
   send(from, "example")
   {:noreply, state}
end

However when I got to write tests for this, the HANDLE INFO sometimes is not called!

    test "things", %{test: test} do
      topic = "#{test}"

      {:ok, pid} = MyGenServer.start_link(%{source: topic, from: self()})

      :ok = PubSub.broadcast(:mypubsub, topic, "example message")

      # some assert_receive here

      # IO.inspect(Process.info(pid, :messages))
    end

I am very confused because I do not see the “HANDLE INFO” when I run my test! However, if I uncomment IO.inspect(Process.info(pid, :messages)) only THEN do I see the message. Can someone explain this? The module otherwise seems to be functioning as expected, it was only when I begin writing tests that I observed this strange thing.

Sounds like your test is returning before the GenServer receives/processes the message.

Calling a synchronous function on the GenServer (any function - can even be :sys.get_state(pid)) should ensure that the message has been processed before the test exits.

3 Likes

Wow. This is … how you say… very big head trip. Thank you for your explanation! This works:

:sys.get_state(pid)
assert_received "output"

Note that this is not really a great way to write your tests.

You should probably write a “dump” function that relays the internal content of your genserver, and call that and assert that the internal state of your genserver is as expected.

I would also strongly question the existence of the “from” parameter in your genserver init, seems like a code smell.

1 Like

yes, this is only a simplification. Really the message describes a job and the message includes information on where to send the result, something like Broadway.

Could you maybe explain a “dump” function? Is it maybe a better to use Process.info(pid, :messages) to assert that messages are received?

That won’t work because by the time you check the message may have been received already, so the message queue will be empty.

I wouldn’t assert on internal state personally. It doesn’t demonstrate that the component behaves correctly, and tying your test too closely to the implementation means it’ll break if you refactor the code (say, rename a variable in the gen_server state) even though the code still works fine. Same reason testing private methods or internal state in OOP is usually a bad idea.

Your original code where the test process sends its own pid as the from then does an assert_received looks great to me. You could even start_link a tiny Broadway pipeline in your test to get a realistic setup if you need to.

Just a tip though though - try assert_receive, rather than assert_received. The former waits for the message to arrive, the latter asserts that the message is already there and fails without waiting if it’s not. With assert_receive you won’t need the sys.get_state trick.

2 Likes