Star date: 2017.222
This topic has been written about a lot, but in spite of that I found myself reading the code in channel_test.ex to figure out exactly how everything fits together. Specifically, I wanted to cover a few things that I couldn't get just from reading this page on hexdocs.
Create a file in test/myapp_webb/channels/my_channel_test.ex
that looks like
this:
defmodule MyAppWeb.ChatChannelTest do
use MyAppWeb.ChannelCase
alias MyAppWeb.ChatChannel
test "test something" do
assert 1 == 1
end
end
then run mix test
just to make sure it's working.
Phoenix.ChannelTest
defines a macro called socket
which makes a fake socket
connection to your channel. It's a real "connection" but not actually a socket.
You can then use this subscribe_and_join
to join the channel and subscribe to
a topic:
test "join channel" do
assert {:ok, _payload, _socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{})
end
The last parameter to subscribe_and_join
is the payload to send in the
phx_join
event (which would be available in the join
function of your
channel) and _payload
is the the response payload from your join
function.
Payloads are often not used when join
ing but you could use it to pass an
authentication token or something if necessary.
For this we use the function push
(all of these functions are from
Phoenix.ChannelTest
btw). So now in a test case we could join the channel and
push an event:
test "join channel and send a message" do
assert {:ok, _payload, socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{ name: "Graeme" })
assert ref = push(socket, "message", "hello")
end
ref
is just the unique id created for this event. Replies from the server
would have a matching ref
.
There are three relevant macros in Phoenix.ChannelTest
but their names can be
a bit confusing since communication is two-way:
assert_push
means "assert that the server pushed an event to me."assert_reply
means "assert that server server replied to my event."assert_broadcast
means "assert that the server broadcasted an event."Replies are pretty simple:
test "receive a reply" do
assert {:ok, _payload, socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{name: "Graeme"})
assert ref = push(socket, "get_others")
assert_reply(ref, :ok, ["John", "Mike", "Sarah"])
end
Push and broadcast are also pretty easy when you are only testing with one client:
test "receive broadcast" do
assert {:ok, _payload, socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{name: "Graeme"})
push(socket, "message", "hello")
assert_broadcast("message", %{from: "Graeme", content: "hello"})
end
If there are multiple clients that we want to test we could do something like this:
test "receive broadcast" do
assert {:ok, _payload, graeme_socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{name: "Graeme" })
assert {:ok, _payload, _john_socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{name: "John"})
assert {:ok, _payload, _sarah_socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{name: "Sarah"})
push(graeme_socket, "message", "hello")
assert_broadcast("message", %{from: "Graeme", content: "hello"})
assert_broadcast("message", %{from: "Graeme", content: "hello"})
assert_broadcast("message", %{from: "Graeme", content: "hello"})
end
That will work, but it's not the best test. All it does is test that it received three broadcasted messages, but there's no way of knowing which client they were intended for. Since the message is sent to a process we need to do each one in its own process:
test "receive broadcast" do
t1 = Task.async(fn ->
assert {:ok, _payload, graeme_socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{name: "Graeme" })
# Wait for the other two processes to be ready. If the message is sent
# before they can join then they won't get it.
receive do :proceed -> :ok end
receive do :proceed -> :ok end
push(graeme_socket, "message", "hello")
assert_broadcast("message", %{from: "Graeme", content: "hello"})
end)
t2 = Task.async(fn ->
assert {:ok, _payload, john_socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{name: "John"})
send(t1.pid, :proceed)
assert_broadcast("message", %{from: "Graeme", content: "hello"})
end)
t3 = Task.async(fn ->
assert {:ok, _payload, sarah_socket} = socket("", %{})
|> subscribe_and_join(ChatChannel, "room:lobby", %{name: "Sarah"})
send(t1.pid, :proceed)
assert_broadcast("message", %{from: "Graeme", content: "hello"})
end)
Enum.map([t1, t2, t3], &Task.await/1)
end