[elixir! #0019][译] 测试Phoenix WebSockets by Alex Jensen

原文

最近我发表了一篇关于如何使用Phoenix的websockets创建一个游戏大厅的文章. 我们团队非常重视测试, 所以, 今天我将介绍如何测试我们之前编写的websocket代码.

准备工作

在开始之前, 我们需要运行 mix test, 先删除一些默认的测试, 并解决测试中的小问题. 当我们编写测试之前, 希望没有其它的错误消息. 接着, 删除所有Phoenix generator 自动生成 channel 测试, 因为我们将要从头开始编写它们.

Helpers

Phoenix已经在你的项目中创建了一些支持文件, 你可以在里面定义常用的测试函数, 类似于其它语言里的 test_helper/spec_helper . 这里有一个针对channel test 的文件 test/support/channel_case.ex , 我们稍后会在其中加入一些代码. 我们在 quote do ... end 结构中添加的代码会被每个 channel test 模块引用. 在这里, 我们想要为 MyApp.User 设置一个别名:

using do
  quote do
    ...
    alias MyApp.{Repo, User}
    ...
  end
end

创建 Lobby Channel Test

现在开始编写测试. 首先创建一个新文件 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

注意, 我们需要 use MyApp.ChannelCase 来从 test/support 文件中引入 helpers.

运行 mix test, 如果一切正确你会收到测试通过的消息.

Socket 测试桩

在测试中模拟用户的socket连接, 可能比你预想的要简单. 在我们的代码里, 用户需要一个token才能连接, 它是在 web/channels/user_socket.ex 文件中进行检查的. 因为我们可以直接创建一个连接到channel的socket, 所以我们不需要通过任何验证. 然而, 我们仍然需要设置一个由验证获得的 current_user 赋值.

使用如下方法创建一个带有 current_user 的socket:

def create_user(username) do
  %User{}
  |> 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})
    |> subscirbe_and_join(LobbyChannel, "game:lobby")
end

这里我们定义了一个使用给定的用户名创建新用户的函数. 这个函数也可以定义在test/support/channel_case.ex 文件中, 但现在先把它留在这里. 创建用户之后, 我们将其传递给 socket 函数, 它是Phoenix引入的函数. 这样就设置好了这个socket的 assigns. 然后将socket 和 channel模块, 以及要加入的房间名一并传入 subscribe_and_join 函数. 如果成功会返回 {:ok, response, socket}, 不过我们没有从大厅系统返回任何值, 所以使用 _ 来忽略.

Assertions

我们已经创建了一个用户并加入了 channel, 但如何判断它是否收到了当前的大厅状态呢? 我们加入的 channel 不会返回一个包含当前用户的回应, 而是在 join 事件之后将数据广播出来. Phoenix 提供了一个用于检查广播的函数 assert_broadcast .

当运行一个 channel test 时, Phoenix会保存一个每次发送的广播的列表, 包含事件名和数据. assert_broadcast 会在这个列表中查找你所期望的广播, 如果找到则返回 true. 如果你期望的广播没有在超时(assert_broadcast函数的第三个参数)之前被发送, 这个assertion 便会失败. 在这里, 我们 assert 广播的事件是 lobby_update , 附带包含了 ["jerry"] 的users:

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

清理

这个 lobby_update 测试现在应该可以通过了, 但还有一个小问题. 在用户加入时会被添加到当前用户列表, 在离开时会被删除, 而我们现在只加入了. 感谢Phoenix提供了一个离开函数, 我们可以这样使用:

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

另一个Helper

在继续之前, 让我们创建另一个 helper 函数来创建一个用户并加入"game:lobby"频道, 因为我们需要在后面的测试中重复这个步骤.

def create_user_and_join_lobby(username) do
  {:ok, user} = create_user(username)

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

现在我们可以将之前的测试改为:

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

测试游戏邀请

为了测试我们的游戏邀请, 我们将创建两个用户, 发送邀请事件到服务器, 并检查是否有邀请发送给用户:

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

如果你去看 "game_invite" 的代码, 你会发现有一个包含发送者和用户名的广播被发出了, 但是广播被拦截了, 只会发送给正确的用户. 在这里, 我们不可以使用 assert_broadcast , 因为我们想要检查的消息并没有被广播. 我们可以使用 assert_push :

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

观察以上代码, 你可能想知道为什么 username 键一会儿是一个字符串, 一会儿是原子. 通常, 你的socke会将所有东西编码成JSON发送给客户端, 这会使所有这些变成字符串. channel 测试拥有对 channel更直接的控制, 所以我们要确保和实际收送的数据完全一致.

确保你离开了每个socket:

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

总结

我们简短地介绍了如何测试 Phoenix websockets. 刚开始你可能认为测试很可怕, 事实上他们很简单快捷. 对于大型应用, 定义强大的 helper 尤为重要. 记得查看 ExUnit 的文档中的各种功能.

相关推荐