Spec-Zone.ru › Phoenix

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, topic)

См. join/4.

join(socket, topic, payload)

См. join/4.

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, topic)

См. subscribe_and_join/4.

subscribe_and_join(socket, topic, payload)

См. subscribe_and_join/4.

subscribe_and_join(socket, channel, topic, payload \\ %{})

Подписывается на указанную тему и присоединяется к каналу с указанными темой и данными.

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 \\ %{})

То же, что и 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!/4.

subscribe_and_join!(socket, topic, payload)Источник

См. subscribe_and_join!/4.

subscribe_and_join!(socket, channel, topic, payload \\ %{})Источник

То же, что и subscribe_and_join/4, но возвращает сокет либо вызывает исключение.

Это полезно, если вы не тестируете присоединение к каналу и вам нужен только сокет.

Пакет Hex Предварительный просмотр Hex Скачать версию ePub

Создано с помощью ExDoc (v0.40.3) для языка программирования Elixir

© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/Phoenix.ChannelTest.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API