Blog

Testing Phoenix Websockets

I recently published this post on creating a game lobby system using Phoenix’s websockets. Here at Quick Left, we place a high value on our tests. This time around, I’ll be going over how to test the websocket code that we created in the previous post.

Starting Fresh

Before you begin, it’s good practice to run mix test and fix any minor errors in tests or delete irrelevant auto generated tests. When we write some tests, we’ll want some clean error messages. Go ahead and delete any channel tests that were created as well if you used Phoenix generators to create your channels, since we’ll be recreating these from the roots up.

Explaining the Helpers

Inside your project, Phoenix has created a set of support files where you can define common test functionality, similar to a test_helper/spec_helper in other languages. For channel testing, there’s a file located at test/support/channel_case.ex which we will be adding some code to. Inside of the quote do...end we can put code that will be included into every channel test module we create. In our case, we will want to be able to call User without the reference to MyApp. Add User to the aliases like so:

# test/support/channel_case.ex
using do
  quote do
    ...
    alias MyApp.{Repo, User}
    ...
  end
end

Creating the Lobby Channel Test

Time to start writing the tests. Start off by making a new file test/channels/lobby_channel_test.exs

# new file
# test/channels/lobby_channel_test.exs
defmodule MyApp.LobbyChannelTest do
  use MyApp.ChannelCase
  alias MyApp.LobbyChannel

  test "give me a dot" do
    assert 1 == 1
  end
end

Notice here that we have to use MyApp.ChannelCase to pull in the helpers from out test/support file.

Go ahead and run mix test here to make sure your files are set up correctly and you receive a dot for your passing test.

Stubbing out the Socket

Stubbing the connected socket of our user in tests is actually much simpler than you may think. In our codebase, users needed a token to be able to connect, which was checked for inside web/channels/user_socket.ex. Because we can create sockets directly onto a channel, we don’t need to stub out any authentication. However, we still need the current_user assign that would have been set by that authentication.

To create a socket with a current_user, use the following:

# test/channels/lobby_channel_test.exs
def create_user(username) do
  %User{}
  # Use the changeset that will match your model
  |> User.changeset(%{username: username, password: "passw0rd", password_confirmation: "passw0rd"})
  |> Repo.insert
end

test "user receives the current list of users online" do
  {:ok, user} = create_user("jerry")
  {:ok, _, socket} = socket("", %{current_user: user})
    |> subscribe_and_join(LobbyChannel, "game:lobby")
end

Here I’m defining a function to create a user from a supplied name. This is a function that can be defined back in test/support/channel_case.ex as well, alongside the User alias, but I’ll leave it here for now. Once we have that user, we pass it with a map to the second argument of the socket function, which is brought in through Phoenix. The map sets the assigns on that socket to be used inside our channel code. We then pass the whole socket into subscribe_and_join with the channel module and room name to be joining. On success, this returns {:ok, response, socket}, but since we don’t respond with anything for the lobby system, use _ to ignore it.

Assertions

Our user has been created and joined the channel, but how do we check that they received the current lobby state? The channel we have set up doesn’t return users with a response including the current users. Instead it broadcasts the data after the join event occurs. To check for broadcasts, Phoenix has a wonderful test helper assert_broadcast function.

As a channel test is run, Phoenix will keep a list of every broadcast that is sent with the event name and the data that was sent along with it. assert_broadcast will look through the list of sent broadcasts and return true if any of them match the assertion you’re making. If the expected broadcast is not sent before the timeout (optional third argument to assert_broadcast), this assertion will fail. For this case, we want to assert that the "lobby_update" event was broadcasted with users: ["jerry"]:

# test/channels/lobby_channel_test.exs
test "user receives the current list of users online" do
  {:ok, user} = create_user("jerry")
  {:ok, _, socket} = socket("", %{current_user: user})
    |> subscribe_and_join(LobbyChannel, "game:lobby")
  assert_broadcast "lobby_update", %{users: ["jerry"]}
end

Cleanup

This lobby_update test should pass for now, but there’s one small problem with it. Our current list of online users is added to on join and subtracted from on leave, but we are currently only joining. Thankfully Phoenix includes a leave function, which we can use like this:

# test/channels/lobby_channel_test.exs
test "user receives the current list of users online" do
  {:ok, user} = create_user("jerry")
  {:ok, _, socket} = socket("", %{current_user: user})
    |> subscribe_and_join(LobbyChannel, "game:lobby")
  assert_broadcast "lobby_update", %{users: ["jerry"]}
  leave socket
end

Another Helper

Before we go any farther, let’s create another helper function to create a user and join the "game:lobby" channel since we’ll be needing to repeat this in the next few tests.

# test/channels/lobby_channel_test.exs
def create_user_and_join_lobby(username) do
  {:ok, user} = create_user(username)

  socket("", %{current_user: user})
  |> subscribe_and_join(LobbyChannel, "game:lobby")
end

Now we can change the last test to:

# test/channels/lobby_channel_test.exs
test "user receives the current list of users online" do
  {:ok, _, socket} = create_user_and_join_lobby("jerry")
  assert_broadcast "lobby_update", %{users: ["jerry"]}
  leave socket
end

Testing Game Invitations

For our game invitations test, we’ll make 2 users, push the invite event to the server, and check that an invite was pushed back to the user:

# test/channels/lobby_channel_test.exs
test "user receives an invite" do
  {:ok, _, socket1} = create_user_and_join_lobby("bill")
  {:ok, _, socket2} = create_user_and_join_lobby("will")
  push socket1, "game_invite", %{"username" => "will"}
end

If you look back at the lobby code for "game_invite", you will see there is a broadcast being sent out with the sender and username, but the broadcast is being intercepted and only sending the correct person the invitation. In this case, we can’t use assert_broadcast since the message we want to check for is not being broadcasted. There is an assert_push for this:

# test/channels/lobby_channel_test.exs
test "user receives an invite" do
  {:ok, _, socket1} = create_user_and_join_lobby("bill")
  {:ok, _, socket2} = create_user_and_join_lobby("will")
  push socket1, "game_invite", %{"username" => "will"}
  assert_push "game_invite", %{username: "bill"}
end

Looking at this, you may be wondering why the “username” key is a string in one spot and an atom in the other. Normally, your socket will push everything encoded in JSON to the client, which would turn these all into strings. The channel tests have more direct access to the channels and have to match exactly the data you accept/push.

Also make sure you leave each socket or we’ll break the previous test depending on the order of tests run:

# test/channels/lobby_channel_test.exs
test "user receives an invite" do
  {:ok, _, socket1} = create_user_and_join_lobby("bill")
  {:ok, _, socket2} = create_user_and_join_lobby("will")
  push socket1, "game_invite", %{"username" => "will"}
  assert_push "game_invite", %{username: "bill"}
  leave socket1
  leave socket2
end

Conclusion

In this post, I’ve given a brief look into how you would go about testing Phoenix websockets. Testing features like this can be very intimidating at first, but are actually quite small and fast. For larger applications, defining powerful test helper functionality becomes much more important. Be sure to check docs for ExUnit apart for these kinds of features.