[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 的文档中的各种功能.