Phoenix.ChannelTest
ИсточникУдобные средства для тестирования каналов Phoenix.
В тестах каналов мы взаимодействуем с каналами посредством обмена сообщениями между процессами, отправляя и получая сообщения. Также часто подписываются на ту же тему, на которую подписан канал, чтобы можно было проверить, было ли передано широковещательное сообщение.
Тестирование каналов
Для начала определите атрибут модуля @endpoint в тестовом модуле, указав в нём конечную точку приложения.
Затем можно напрямую создать сокет и присоединиться к темам и каналам с помощью subscribe_and_join/4:
{:ok, _, socket} =
socket(UserSocket, "user:id", %{some_assigns: 1})
|> subscribe_and_join(RoomChannel, "room:lobby", %{"id" => 3})
Обычно следует задать тот же идентификатор и те же назначения, которые установил бы ваш обработчик обратного вызова UserSocket.connect/3. Кроме того, можно воспользоваться вспомогательной функцией connect/3, чтобы вызвать обработчик обратного вызова UserSocket.connect/3 и инициализировать сокет с идентификатором сокета:
{:ok, socket} = connect(UserSocket, %{"some" => "params"}, %{})
{:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{"id" => 3})
После вызова subscribe_and_join/4 подпишет текущий тестовый процесс на тему "room:lobby" и запустит канал в другом процессе. Функция возвращает {:ok, reply, socket} или {:error, reply}.
Подобно тому как у канала есть сокет, представляющий связь с клиентом, которому он будет отправлять сообщения, у нашего теста есть сокет, представляющий связь с сервером, которому будут отправляться сообщения.
Например, в тесте можно использовать функцию push/3, чтобы отправлять сообщения каналу (она вызовет handle_in/3):
push(socket, "my_event", %{"some" => "data"})
Аналогичным образом тест может самостоятельно передавать широковещательные сообщения по теме, на которую подписаны и тест, и канал, вызывая в канале handle_out/3:
broadcast_from(socket, "my_event", %{"some" => "data"})
Обратите внимание: в тестах доступны только
broadcast_from/3иbroadcast_from!/3, чтобы широковещательные сообщения не отправлялись повторно тестовому процессу.
Пока функции выше отправляют данные каналу (серверу), с помощью assert_push/3 можно проверить, что канал отправил сообщение клиенту:
assert_push "my_event", %{"some" => "data"}
Можно также проверить, что сообщение было передано через pubsub:
assert_broadcast "my_event", %{"some" => "data"}
Наконец, при каждой отправке сообщения каналу возвращается ссылка. С её помощью можно проверить, что сервер отправил определённый ответ:
ref = push(socket, "counter", %{})
assert_reply ref, :ok, %{"counter" => 1}
Проверка побочных эффектов
Часто внутри каналов требуется выполнять побочные эффекты, например записывать данные в базу данных, и проверять эти побочные эффекты в тестах.
Представим следующую handle_in/3 внутри канала:
def handle_in("publish", %{"id" => id}, socket) do
Repo.get!(Post, id) |> Post.publish() |> Repo.update!()
{:noreply, socket}
end
Поскольку обмен сообщениями выполняется асинхронно, следующий тест будет ненадёжным:
push(socket, "publish", %{"id" => 3})
assert Repo.get_by(Post, id: 3, published: true)
Проблема в том, что после вызова push/3 нет гарантии, что канал уже обработал наше сообщение. Оптимальное решение — проверить, что канал отправил ответ, прежде чем выполнять другие проверки. Сначала изменим канал, чтобы он отправлял ответы:
def handle_in("publish", %{"id" => id}, socket) do
Repo.get!(Post, id) |> Post.publish() |> Repo.update!()
{:reply, :ok, socket}
end
Затем будем ожидать их в тесте:
ref = push(socket, "publish", %{"id" => 3})
assert_reply ref, :ok
assert Repo.get_by(Post, id: 3, published: true)
Выход и закрытие
Этот модуль также предоставляет функции для имитации выхода из канала и его закрытия. Поскольку при присоединении канал связывается с тестовым процессом, после выхода из канала или его закрытия тестовый процесс завершится аварийно:
leave(socket)
** (EXIT from #PID<...>) {:shutdown, :leave}
Чтобы этого избежать, можно разорвать связь с процессом канала в тесте:
Process.unlink(socket.channel_pid)
Обратите внимание: leave/1 работает асинхронно, поэтому она также возвращает ссылку, которую можно использовать для проверки ответа:
ref = leave(socket) assert_reply ref, :ok
С другой стороны, закрытие всегда выполняется синхронно и возвращается только после того, как процесс канала гарантированно завершён:
:ok = close(socket)
Это имитирует поведение, реализованное в клиентах.
Чтобы проверить, что канал асинхронно закрывается или завершается с ошибкой, можно отслеживать процесс канала с помощью средств Elixir и ожидать сообщение :DOWN. Представим реализацию функции handle_info/2, которая закрывает канал при получении :some_message:
def handle_info(:some_message, socket) do
{:stop, :normal, socket}
end
В тесте можно проверить, что канал закрылся, следующим образом:
Process.monitor(socket.channel_pid)
send(socket.channel_pid, :some_message)
assert_receive {:DOWN, _, _, _, :normal} Сводка
- assert_broadcast(event, payload, timeout \\ Application.fetch_env!(:ex_unit, :assert_receive_timeout))
Проверяет, что канал передал широковещательное сообщение в течение
timeout.- assert_push(event, payload, timeout \\ Application.fetch_env!(:ex_unit, :assert_receive_timeout))
Проверяет, что канал отправил клиенту сообщение с указанными событием и данными в течение
timeout.- assert_reply(ref, status, payload \\ Macro.escape(%{}), timeout \\ Application.fetch_env!(:ex_unit, :assert_receive_timeout))
Проверяет, что канал ответил на указанное сообщение в течение
timeout.- broadcast_from(socket, event, message)
Передаёт событие от процесса всем подписчикам темы сокета.
- broadcast_from!(socket, event, message)
То же, что и
broadcast_from/3, но вызывает исключение, если передача сообщения завершается неудачно.- close(socket, timeout \\ 5000)
Имитирует закрытие сокета клиентом.
- connect(handler, params, options \\ quote do [] end)
Устанавливает транспортное соединение для обработчика сокета.
- join(socket, channel, topic, payload \\ %{})
Присоединяется к каналу с указанными темой и данными.
- leave(socket)
Имитирует выход клиента из канала.
- push(socket, event, payload \\ %{})
Отправляет сообщение в канал.
- refute_broadcast(event, payload, timeout \\ Application.fetch_env!(:ex_unit, :refute_receive_timeout))
Проверяет, что канал не передавал широковещательное сообщение в течение
timeout.- refute_push(event, payload, timeout \\ Application.fetch_env!(:ex_unit, :refute_receive_timeout))
Проверяет, что канал не отправлял клиенту сообщение с указанными событием и данными в течение
timeout.- refute_reply(ref, status, payload \\ Macro.escape(%{}), timeout \\ Application.fetch_env!(:ex_unit, :refute_receive_timeout))
Проверяет, что канал не отправлял ответ с соответствующими данными в течение
timeout.- socket(socket_module)
Создаёт сокет для указанного
socket_module.- socket(socket_module, socket_id, socket_assigns, options \\ [])
Создаёт сокет для указанного
socket_moduleс заданными идентификатором и назначениями.- subscribe_and_join(socket, channel, topic, payload \\ %{})
Подписывается на указанную тему и присоединяется к каналу с указанными темой и данными.
- subscribe_and_join!(socket, channel, topic, payload \\ %{})
То же, что и
subscribe_and_join/4, но возвращает сокет либо вызывает исключение.
Функции
Функции
assert_broadcast(event, payload, timeout \\ Application.fetch_env!(:ex_unit, :assert_receive_timeout))Источник
Проверяет, что канал передал широковещательное сообщение в течение timeout.
Прежде чем проверять, было ли передано сообщение, необходимо сначала подписать тестовый процесс на тему канала:
@endpoint.subscribe("foo:ok")
Теперь можно сопоставлять событие и данные с помощью шаблонов:
assert_broadcast "some_event", %{"data" => _}
В приведённой выше проверке нас не особенно интересуют передаваемые данные — важно лишь, что что-то было отправлено.
Тайм-аут задаётся в миллисекундах. По умолчанию используется значение :assert_receive_timeout, заданное для приложения :ex_unit (по умолчанию — 100 мс).
Для шаблона данных также можно указать защитные условия:
assert_broadcast "some_event", %{"counter" => c} when c > 0 assert_push(event, payload, timeout \\ Application.fetch_env!(:ex_unit, :assert_receive_timeout))Источник
Проверяет, что канал отправил клиенту сообщение с указанными событием и данными в течение timeout.
Обратите внимание: событие и данные являются шаблонами. Это позволяет, например, написать:
assert_push "some_event", %{"data" => _}
В приведённой выше проверке нас не особенно интересуют передаваемые данные — важно лишь, что что-то было отправлено.
Тайм-аут задаётся в миллисекундах. По умолчанию используется значение :assert_receive_timeout, заданное для приложения :ex_unit (по умолчанию — 100 мс).
ПРИМЕЧАНИЕ: Событие и данные являются шаблонами, поэтому для них выполняется сопоставление. Это означает, что если нужно проверить соответствие полученных данных существующей переменной, в выражении проверки необходимо закрепить переменную.
Правильно:
expected_payload = %{foo: "bar"}
assert_push "some_event", ^expected_payload
Неправильно:
expected_payload = %{foo: "bar"}
assert_push "some_event", expected_payload
# The code above does not assert the payload matches the described map.
Для шаблона данных также можно указать защитные условия:
assert_push "some_event", %{"counter" => c} when c > 0 assert_reply(ref, status, payload \\ Macro.escape(%{}), timeout \\ Application.fetch_env!(:ex_unit, :assert_receive_timeout))Источник
Проверяет, что канал ответил на указанное сообщение в течение timeout.
Обратите внимание: статус и данные являются шаблонами. Это позволяет, например, написать:
ref = push(channel, "some_event")
assert_reply ref, :ok, %{"data" => _}
В приведённой выше проверке нас не особенно интересуют передаваемые данные — важно лишь, что был получен ответ.
Тайм-аут задаётся в миллисекундах. По умолчанию используется значение :assert_receive_timeout, заданное для приложения :ex_unit (по умолчанию — 100 мс).
Для шаблона данных также можно указать защитные условия:
ref = push(channel, "some_event")
assert_reply ref, :ok, %{"counter" => c} when c > 0 broadcast_from(socket, event, message)Источник
Передаёт событие от процесса всем подписчикам темы сокета.
Тестовый процесс не получит опубликованное сообщение. Это вызывает в канале функцию обратного вызова handle_out/3.
Примеры
iex> broadcast_from(socket, "new_message", %{id: 1, content: "hello"})
:ok broadcast_from!(socket, event, message)Источник
То же, что и broadcast_from/3, но вызывает исключение, если передача сообщения завершается неудачно.
close(socket, timeout \\ 5000)Источник
Имитирует закрытие сокета клиентом.
По умолчанию это приведёт к аварийному завершению тестового процесса. Чтобы этого избежать, перед вызовом выполните Process.unlink(socket.channel_pid). См. раздел «Выход и закрытие».
Закрытие сокета выполняется синхронно. Тайм-аут по умолчанию составляет 5000 миллисекунд.
connect(handler, params, options \\ quote do [] end)Источник
Устанавливает транспортное соединение для обработчика сокета.
Полезно для тестирования аутентификации UserSocket. Возвращает результат функции обратного вызова connect/3 обработчика.
join(socket, topic)Источник
См. join/4.
join(socket, topic, payload)Источник
См. join/4.
join(socket, channel, topic, payload \\ %{})Источник
Присоединяется к каналу с указанными темой и данными.
Указанный канал присоединяется в отдельном процессе, связанном с тестовым процессом.
Функция возвращает {:ok, reply, socket} или {:error, reply}.
leave(socket)Источник
@spec leave(Phoenix.Socket.t()) :: reference()
Имитирует выход клиента из канала.
По умолчанию это приведёт к аварийному завершению тестового процесса. Чтобы этого избежать, перед вызовом выполните Process.unlink(socket.channel_pid). См. раздел «Выход и закрытие».
push(socket, event, payload \\ %{})Источник
@spec push(Phoenix.Socket.t(), String.t(), map()) :: reference()
Отправляет сообщение в канал.
Это вызывает в канале функцию обратного вызова handle_in/3.
Примеры
iex> push(socket, "new_message", %{id: 1, content: "hello"})
reference refute_broadcast(event, payload, timeout \\ Application.fetch_env!(:ex_unit, :refute_receive_timeout))Источник
Проверяет, что канал не передавал широковещательное сообщение в течение timeout.
Как и в assert_broadcast, событие и данные являются шаблонами.
Тайм-аут задаётся в миллисекундах. По умолчанию используется значение :refute_receive_timeout, заданное для приложения :ex_unit (по умолчанию — 100 мс). Помните, что этот макрос блокирует выполнение теста на время, заданное тайм-аутом. Используйте его только при необходимости: чрезмерное использование значительно замедлит весь набор тестов.
refute_push(event, payload, timeout \\ Application.fetch_env!(:ex_unit, :refute_receive_timeout))Источник
Проверяет, что канал не отправлял клиенту сообщение с указанными событием и данными в течение timeout.
Как и в assert_push, событие и данные являются шаблонами.
Тайм-аут задаётся в миллисекундах. По умолчанию используется значение :refute_receive_timeout, заданное для приложения :ex_unit (по умолчанию — 100 мс). Помните, что этот макрос блокирует выполнение теста на время, заданное тайм-аутом. Используйте его только при необходимости: чрезмерное использование значительно замедлит весь набор тестов.
refute_reply(ref, status, payload \\ Macro.escape(%{}), timeout \\ Application.fetch_env!(:ex_unit, :refute_receive_timeout))Источник
Проверяет, что канал не отправлял ответ с соответствующими данными в течение timeout.
Как и в assert_reply, событие и данные являются шаблонами.
Тайм-аут задаётся в миллисекундах. По умолчанию используется значение :refute_receive_timeout, заданное для приложения :ex_unit (по умолчанию — 100 мс). Помните, что этот макрос блокирует выполнение теста на время, заданное тайм-аутом. Используйте его только при необходимости: чрезмерное использование значительно замедлит весь набор тестов.
socket(socket_module)Источник
Создаёт сокет для указанного socket_module.
Затем сокет используется для подписки на каналы и присоединения к ним. Используйте эту функцию, чтобы создать пустой сокет для передачи в такие функции, как UserSocket.connect/3.
В противном случае используйте socket/4, если нужно создать сокет с уже заданными идентификатором и назначениями.
Примеры
socket(MyApp.UserSocket)
socket(socket_module, socket_id, socket_assigns, options \\ [])Источник
Создаёт сокет для указанного socket_module с заданными идентификатором и назначениями.
Примеры
socket(MyApp.UserSocket, "user_id", %{some: :assign})
Если сокет нужно использовать в другом процессе, а не в тестовом, в четвёртом аргументе можно передать pid тестового процесса.
Примеры
test "connect in a task" do
pid = self()
task = Task.async(fn ->
socket = socket(MyApp.UserSocket, "user_id", %{some: :assign}, test_process: pid)
broadcast_from!(socket, "default", %{"foo" => "bar"})
assert_push "default", %{"foo" => "bar"}
end)
Task.await(task)
end subscribe_and_join(socket, topic)Источник
См. subscribe_and_join/4.
subscribe_and_join(socket, topic, payload)Источник
См. subscribe_and_join/4.
subscribe_and_join(socket, channel, topic, payload \\ %{})Источник
Подписывается на указанную тему и присоединяется к каналу с указанными темой и данными.
Подписка на тему позволяет использовать assert_broadcast/3, чтобы проверить, было ли сообщение отправлено через уровень pubsub.
Присоединившись к каналу, можно взаимодействовать с ним напрямую. Указанный канал присоединяется в отдельном процессе, связанном с тестовым процессом.
Если модуль канала не указан, обработчик сокета используется для поиска канала, соответствующего указанной теме.
Функция возвращает {:ok, reply, socket} или {:error, reply}.
subscribe_and_join!(socket, topic)Источник
subscribe_and_join!(socket, topic, payload)Источник
subscribe_and_join!(socket, channel, topic, payload \\ %{})Источник
То же, что и subscribe_and_join/4, но возвращает сокет либо вызывает исключение.
Это полезно, если вы не тестируете присоединение к каналу и вам нужен только сокет.
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/Phoenix.ChannelTest.html