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:
- client1 joins room
- client2 joins room
- client1 shouts “hi”
- 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 call
ing 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.