Jeff Cole

I build things with computers, for people.

Testing Phoenix Sockets and Channels

October 12, 2016

This post is one in a series about building a collaborative music app in Elixir and Elm called Loops With Friends. If you'd like to catch up, visit the first post in the series to learn all about it!

In the first post of this series we introduced the Loops With Friends app, and implemented channel joining with Phoenix. Next we looked at presence tracking, loop cycling, and event broadcasting. Then we added jam balancing by leveraging an Elixir agent, and fixed a race condition in our channel joining process.

The next few posts will look at how the functional nature of Elixir makes applications written in it a joy to test. Elixir ships with the unit testing framework ExUnit, which provides great testing support out of the box. We'll start with a brief look at testing Phoenix's abstractions around sockets and channels, and then move on to some techniques for building and maintaining a healthy test suite in Elixir.

Making use of ChannelTest

Along with the code to support channel implementation, Phoenix also ships with code to make testing sockets and channels straightforward. This code lives in the Phoenix.ChannelTest module. The way that we take advantage of it is to use it in a module of our own, and then use that module in our actual test module. Note that this process is analogous to how we implemented presence.

Specifically, we use Phoenix.ChannelTest in our LoopsWithFriends submodule ChannelCase, and then use LoopsWithFriends.ChannelCase in both of our UserSocketTest and JamChannelTest modules. Once we've done so, we have access to all of the functions and macros that Phoenix.ChannelTest provides. For instance, in UserSocketTest, we use the connect macro to initialize a socket that we can then assert against.

# test/channels/user_socket_test.exs
defmodule LoopsWithFriends.UserSocketTest do
  use LoopsWithFriends.ChannelCase, async: true

  alias LoopsWithFriends.UserSocket

  test "`connect` assigns a UUID" do
    assert {:ok, socket} = connect(UserSocket, %{})

    # Ensure a valid UUID
    assert UUID.info!(socket.assigns.user_id)
  end
end

The async: true on the use line lets ExUnit know that the tests in this module are safe to run asynchronously, which is a huge workflow boon for anyone familiar with slow test suites.

The tests in JamChannelTest similarly take advantage of Phoenix test functions and macros such as subscribe_and_join, push, assert_push, refute_push, and assert_broadcast.

# test/channels/jam_channel_test.exs
defmodule LoopsWithFriends.JamChannelTest do
  use LoopsWithFriends.ChannelCase, async: true

  setup do
    {:ok, socket} = connect(LoopsWithFriends.UserSocket, %{})

    {:ok, socket: socket}
  end

  describe "`join`" do
    test "replies with a `user_id`", %{socket: socket} do
      {:ok, reply, _socket} =
        subscribe_and_join(socket, "jams:jam-1", %{})

      assert %{user_id: user_id} = reply
      assert user_id
    end

    test "assigns the `jam_id`", %{socket: socket} do
      socket = subscribe_and_join!(socket, "jams:jam-1", %{})

      assert socket.assigns.jam_id == "jam-1"
    end

    test "pushes presence state", %{socket: socket} do
      {:ok, _reply, _socket} =
        subscribe_and_join(socket, "jams:jam-1", %{})

      assert_push "presence_state", %{}
    end
  end

  # ...
end

These conveniences make it easy to hook into the lifecycle of our channel and verify its behavior. Check out the JamChannelTest source to see the module tested in its entirety, including test cases covering a full jam and event broadcasting.

In the next post we'll move on to testing the components of our application that make it unique.