Phoenix (Elixir)MiddleTechnical

Как тестировать LiveView flows с асинхронными событиями, uploads и reconnect сценариями?

Phoenix.LiveViewTest позволяет тестировать async handle_info через send/2, загрузки через file_input/render_upload, а reconnect — пересозданием live/2. Все операции синхронизируются автоматически без Process.sleep.

Тестирование LiveView: async события, uploads, reconnect

Phoenix LiveView предоставляет Phoenix.LiveViewTest — библиотеку для синхронного тестирования асинхронных LiveView-потоков без реального браузера.

Базовая структура теста

defmodule MyAppWeb.RoomLiveTest do
  use MyAppWeb.ConnCase, async: true
  import Phoenix.LiveViewTest

  test "отображает сообщения", %{conn: conn} do
    {:ok, lv, html} = live(conn, ~p"/rooms/1")
    assert html =~ "Welcome"

    # Симуляция события от пользователя
    lv
    |> form("#message-form", message: %{body: "Hello"})
    |> render_submit()

    assert render(lv) =~ "Hello"
  end
end

Асинхронные события (handle_info)

Phoenix.LiveViewTest обрабатывает handle_info синхронно при вызове через PubSub из теста. Для прямой отправки используйте send/2:

test "обновляет список при новом сообщении", %{conn: conn} do
  {:ok, lv, _html} = live(conn, ~p"/rooms/1")

  # Симуляция PubSub-события
  send(lv.pid, {:new_message, %{body: "Async message", user: "Alice"}})

  # render/1 ждёт завершения handle_info
  assert render(lv) =~ "Async message"
end

test "broadcast обновляет LiveView", %{conn: conn} do
  {:ok, lv, _html} = live(conn, ~p"/rooms/1")

  # Прямой broadcast через PubSub
  Phoenix.PubSub.broadcast(MyApp.PubSub, "room:1", {:new_message, %{body: "PubSub msg"}})

  assert render(lv) =~ "PubSub msg"
end

Тестирование uploads

file_input/4 симулирует выбор файла; render_upload/3 загружает его:

test "загружает аватар", %{conn: conn} do
  {:ok, lv, _html} = live(conn, ~p"/profile")

  avatar =
    file_input(lv, "#upload-form", :avatar, [
      %{
        last_modified: 1_594_171_879_000,
        name: "my_avatar.jpg",
        content: File.read!("test/fixtures/avatar.jpg"),
        type: "image/jpeg"
      }
    ])

  # Рендерим загрузку (симулирует chunk upload)
  assert render_upload(avatar, "my_avatar.jpg") =~ "100%"

  # Сабмит формы с загруженным файлом
  lv
  |> form("#upload-form")
  |> render_submit()

  assert render(lv) =~ "Avatar updated"
end

test "отклоняет файл превышающий лимит", %{conn: conn} do
  {:ok, lv, _html} = live(conn, ~p"/profile")

  oversized =
    file_input(lv, "#upload-form", :avatar, [
      %{name: "big.jpg", content: :crypto.strong_rand_bytes(10_000_000), type: "image/jpeg"}
    ])

  assert [%{valid?: false, errors: [:too_large]}] = oversized.entries
end

Reconnect сценарии

LiveViewTest позволяет симулировать переподключение через :reconnect:

test "восстанавливает данные после reconnect", %{conn: conn} do
  {:ok, lv, _html} = live(conn, ~p"/dashboard")

  # Обновляем state
  send(lv.pid, {:data_updated, %{count: 42}})
  assert render(lv) =~ "42"

  # Симулируем reconnect — создаёт новый LiveView-процесс
  {:ok, lv2, reconnect_html} = live(conn, ~p"/dashboard")

  # Данные из URL/сессии должны восстановиться
  assert reconnect_html =~ "Dashboard"
end

Тестирование LiveComponent

test "компонент обновляет родительский LiveView", %{conn: conn} do
  {:ok, lv, _html} = live(conn, ~p"/rooms/1")

  # Клик в компоненте
  lv
  |> element("[phx-target][phx-click=\"like\"]", "Like")
  |> render_click()

  assert render(lv) =~ "Liked!"
end

Async assigns (LiveView 0.19+)

test "async assign загружает данные", %{conn: conn} do
  {:ok, lv, _} = live(conn, ~p"/profile/1")

  # Ждём завершения async assign
  assert render(lv) =~ "loading"

  # LiveViewTest автоматически дожидается async операций
  assert render(lv) =~ "User Name"
end

Подводные камни

  • Process.sleep в тестах. Никогда не используйте Process.sleep для ожидания async операций. render/1 сам синхронизируется с процессом LiveView. Sleep маскирует race conditions.
  • ConnCase vs ChannelCase. LiveView тесты используют ConnCase, не ChannelCase. Смешение приводит к ошибкам подключения.
  • file_input с большими файлами. render_upload загружает весь файл в память теста. Используйте минимальные фикстуры; не копируйте production-файлы в test/fixtures.
  • Навигация между LiveView. Для тестирования live_patch и live_redirect используйте follow_trigger_action/2 или assert_patch/2, не пересоздавайте conn.
  • Shared state в async: true тестах. При async: true убедитесь, что тесты не разделяют глобальный ETS или процессы. Используйте уникальные topic-строки с unique_integer/0.
  • handle_info от внешних систем. Если LiveView подписан на внешний источник (Phoenix.Tracker, Broadway), в тестах изолируйте подписку через mock или тестовые топики.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics