Phoenix.ChannelTest

Parsed documentation:
View on GitHub
Conveniences for testing Phoenix channels.

In channel tests, we interact with channels via process
communication, sending and receiving messages. It is also
common to subscribe to the same topic the channel subscribes
to, allowing us to assert if a given message was broadcast
or not.

## Channel testing

To get started, define the module attribute `@endpoint`
in your test case pointing to your application endpoint.

Then you can directly create a socket and
`subscribe_and_join/4` topics and channels:

    {:ok, _, socket} =
      socket("user:id", %{some_assigns: 1})
      |> subscribe_and_join(RoomChannel, "room:lobby", %{"id" => 3})

You usually want to set the same ID and assigns your
`UserSocket.connect/3` callback would set. Alternatively,
you can use the `connect/3` helper to call your `UserSocket.connect/3`
callback and initialize the socket with the socket id:

    {:ok, socket} = connect(UserSocket, %{"some" => "params"}, %{})
    {:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{"id" => 3})

Once called, `subscribe_and_join/4` will subscribe the
current test process to the "room:lobby" topic and start a
channel in another process. It returns `{:ok, reply, socket}`
or `{:error, reply}`.

Now, in the same way the channel has a socket representing
communication it will push to the client. Our test has a
socket representing communication to be pushed to the server.

For example, we can use the `push/3` function in the test
to push messages to the channel (it will invoke `handle_in/3`):

    push(socket, "my_event", %{"some" => "data"})

Similarly, we can broadcast messages from the test itself
on the topic that both test and channel are subscribed to,
triggering `handle_out/3` on the channel:

    broadcast_from(socket, "my_event", %{"some" => "data"})

> Note only `broadcast_from/3` and `broadcast_from!/3` are
available in tests to avoid broadcast messages to be resent
to the test process.

While the functions above are pushing data to the channel
(server) we can use `assert_push/3` to verify the channel
pushed a message to the client:

    assert_push "my_event", %{"some" => "data"}

Or even assert something was broadcast into pubsub:

    assert_broadcast "my_event", %{"some" => "data"}

Finally, every time a message is pushed to the channel,
a reference is returned. We can use this reference to
assert a particular reply was sent from the server:

    ref = push(socket, "counter", %{})
    assert_reply ref, :ok, %{"counter" => 1}

## Checking side-effects

Often one may want to do side-effects inside channels,
like writing to the database, and verify those side-effects
during their tests.

Imagine the following `handle_in/3` inside a channel:

    def handle_in("publish", %{"id" => id}, socket) do
      Repo.get!(Post, id) |> Post.publish() |> Repo.update!()
      {:noreply, socket}
    end

Because the whole communication is asynchronous, the
following test would be very brittle:

    push(socket, "publish", %{"id" => 3})
    assert Repo.get_by(Post, id: 3, published: true)

The issue is that we have no guarantees the channel has
done processing our message after calling `push/3`. The
best solution is to assert the channel sent us a reply
before doing any other assertion. First change the
channel to send replies:

    def handle_in("publish", %{"id" => id}, socket) do
      Repo.get!(Post, id) |> Post.publish() |> Repo.update!()
      {:reply, :ok, socket}
    end

Then expect them in the test:

    ref = push(socket, "publish", %{"id" => 3})
    assert_reply ref, :ok
    assert Repo.get_by(Post, id: 3, published: true)

## Leave and close

This module also provides functions to simulate leaving
and closing a channel. Once you leave or close a channel,
because the channel is linked to the test process on join,
it will crash the test process:

    leave(socket)
    ** (EXIT from #PID<...>) {:shutdown, :leave}

You can avoid this by unlinking the channel process in
the test:

    Process.unlink(socket.channel_pid)

Notice `leave/1` is async, so it will also return a
reference which you can use to check for a reply:

    ref = leave(socket)
    assert_reply ref, :ok

On the other hand, close is always sync and it will
return only after the channel process is guaranteed to
have been terminated:

    :ok = close(socket)

This mimics the behaviour existing in clients.

To assert that your channel closes or errors asynchronously,
you can monitor the channel process with the tools provided
by Elixir, and wait for the `:DOWN` message.
Imagine an implementation of the `handle_info/2` function
that closes the channel when it receives `:some_message`:

    def handle_info(:some_message, socket) do
      {:stop, :normal, socket}
    end

In your test, you can assert that the close happened by:

    Process.monitor(socket.channel_pid)
    send(socket.channel_pid, :some_message)
    assert_receive {:DOWN, _, _, _, :normal}
No suggestions.
Please help! Open an issue on GitHub if this assessment is incorrect.