1

原文

最近我发表了一篇关于如何使用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 的文档中的各种功能.


Ljzn
399 声望102 粉丝

网络安全;函数式编程;数字货币;人工智能