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.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.