Testing a phoenix channel with multiple clients

Hi I’m just starting out with Elixir and Phoenix so I want to see if I’m going about this the right way. Let’s say I have a Phoenix Channel that acts as a chat client. All users that are in the same room/topic can either send messages directly to one another, or “shout” them so that everyone gets the message. I can make a test for shout like this:

# replaced some unimportant params with ... to keep this small 
test "shout" do
  assert {:ok, _, socket1} = socket("", %{}) |> subscribe_and_join(...)
  assert {:ok, _, _socket2} = socket("", %{}) |> subscribe_and_join(...)

  push(socket1, "shout", "hi")

  assert_broadcast("shout", %{msg: "hi"})
  assert_broadcast("shout", %{msg: "hi"})
end

But the issue is that this only tests that the message got sent twice. If somehow it got broadcasted twice but only to one of the clients the test would still pass. Since clients are identified by their pid it looks like the only way to differentiate is to spawn a new process for each client. I really want the test to be this:

  1. client1 joins room
  2. client2 joins room
  3. client1 shouts “hi”
  4. client1 receives “hi” shout and client2 receives “hi” shout (order not important)

It’s sort of ugly but I can do that like this:

test "shout2" do
  task1 = Task.async(fn ->
    assert {:ok, _, socket1} = socket("", %{}) |> subscribe_and_join(...)
    receive do :proceed -> :ok end # make sure client2 has joined before shouting
    push(socket1, "shout", "hi")
    assert_broadcast("shout", %{msg: "hi"})
  end)

  task2 = Task.async(fn ->
    assert {:ok, _, _socket2} = socket("", %{}) |> subscribe_and_join(...)
    send(task1.pid, :proceed)
    assert_broadcast("shout", %{msg: "hi"})
  end)

  Enum.map([task1, task2], &Task.await/1)
end

That works, but it seems like it could become a pain to write tests like this especially since my real use case is more complicated. To simplify things I made a GenServer called AsyncSocket that allows me to write the test like this:

test "shout3" do
  s1 = AsyncSocket.create_and_join(...)
  s2 = AsyncSocket.create_and_join(...)

  AsyncSocket.push s1, "shout", "hi"
  AsyncSocket.assert_broadcast s1, "shout", %{msg: "hi"}
  AsyncSocket.assert_broadcast s2, "shout", %{msg: "hi"}
end

AsyncSocket.push and AsyncSocket.assert_broadcast end up calling the GenServer so that those functions can run on the same process that owns the socket.

So my question is this: Am I going in the right direction? I feel like I must be reinventing the wheel but I can’t figure out how to do this cleanly using the libraries built into Phoenix.

EDIT: here is a gist with the source of AsyncSocket in case its not clear what it does.

If you’re not using intercept, I would say there’s no need to test with multiple channels. A broadcast that’s not intercepted will go to all clients by definition.

If you are using intercept, then you need to use assert_push instead of assert_broadcast. Otherwise the channel could filter the broadcast in its handle_out callback and the test would not catch it.

You can avoid using separate processes by testing the two properties separately:

test "shouts are broadcasted exactly once" do
  assert {:ok, _, socket} = socket("", %{}) |> subscribe_and_join(...)

  push(socket, "shout", "hi")
  assert_broadcast("shout", %{msg: "hi"})
  refute_broadcast("shout", %{msg: "hi"})
end

test "shouts are pushed to all clients" do
  assert {:ok, _, socket1} = socket("", %{}) |> subscribe_and_join(...)
  assert {:ok, _, _socket2} = socket("", %{}) |> subscribe_and_join(...)

  push(socket1, "shout", "hi")

  assert_push("shout", %{msg: "hi"})
  assert_push("shout", %{msg: "hi"})
end
1 Like

Yes I will be using intercept (but I did not do iot in the test code I wrote for these examples) so I guess I oversimplified. I didn’t know I need to use assert_push in that case. Thanks for clearing that up.

I was trying to make this representative of a more general problem when you need to assert that specific clients receive specific messages. For example, if you could send a group message to a specific list of people and you needed to test that the correct clients received it and the others did not receive it then you would need multiple processes, right?

It depends how your app works, but in general you only need one process (the receiver) to test the filtering. The test itself acts as the sender using broadcast_from.

If you really need to have multiple clients in parallel, then I think the AsyncSocket sounds reasonable.