LiveView 怎么测:从组件测试到端到端,我终于不再只敢手点页面了

前言

我最早写 LiveView 的时候,测试意识其实很飘。

原因也不复杂。

一方面,LiveView 给人的第一印象太容易让人放松警惕了。页面交互写在服务端,状态不用前后端来回对齐,很多改动我一边写一边点,浏览器里看着都挺正常。那种感觉特别像:

"这玩意儿都已经省了我半个前端了,总不至于测试还跟以前一样折磨人吧。"

另一方面,我之前被 SPA 测试教育过。

不是说 React、Vue 不能测,而是它们很多时候会把你拖进一个很熟悉的泥潭:

  • 组件能不能挂起来是一回事
  • 状态管理要不要 mock 是一回事
  • 路由要不要补上下文是一回事
  • 接口、定时器、浏览器行为又是另外几回事

测到后面,经常有一种说不清的疲惫:我到底是在验证功能,还是在给测试环境补基础设施?

LiveView 在这点上确实讨喜很多。它更像是让你直接对着"用户看到的页面"和"用户触发的事件"下手,心智负担比典型 SPA 测试小不少。

但我后面才反应过来,LiveView 测试不是没有难点,它只是把难点换成了另一种风格:

  • 你测的是服务端持有状态的页面
  • 你碰到的是进程消息、广播、重连、patch
  • 你要防的不是前端状态漂移,而是"这个页面明明能开,结果一交互就露馅"

所以我现在对 LiveView 测试的理解很简单:

它不是更低级,也不是更高级,它只是更适合拿"用户动作"当测试单位。

这个视角一换,很多事情就顺了。

1. 我后来最依赖的,不是测内部实现,而是 mount -> render -> element -> assert

我一开始写测试,最大的问题不是不会用 LiveViewTest,而是总想偷看内部。

比如想直接断言 assign,想跳过 DOM 直接打某个函数,想尽快证明"代码逻辑没问题"。这种冲动很正常,毕竟这样写起来快。

但 LiveView 这东西,一旦太早绕开页面,你很容易测着测着就偏了。

因为用户又不会盯着你的 socket assign 过日子。用户只会干三件事:

  1. 打开页面
  2. 点东西、填东西
  3. 看页面有没有按预期变化

所以我后来越来越喜欢一条很土的路径:

mount -> render -> element -> assert

比如一个最普通的计数器页面,我现在更愿意这么测:

elixir 复制代码
test "点击加一后页面数字会更新", %{conn: conn} do
  {:ok, view, html} = live(conn, ~p"/counter")

  assert html =~ "当前值:0"

  html =
    view
    |> element("button#inc")
    |> render_click()

  assert html =~ "当前值:1"
  assert render(view) =~ "当前值:1"
end

这段测试的价值,不在于它有多高级,而在于它测的是一整段真实交互:

  • 页面能不能挂起来
  • DOM 上是不是真的有这个按钮
  • 这个按钮是不是绑定了正确事件
  • 事件发生后,用户看到的结果有没有变

这比"我知道 handle_event/3 里 count 会加一"有用得多。

因为后者只是你脑内的逻辑成立,前者才是页面真的活着。

还有一个我后来特别认同的小习惯:能用 element(...) |> render_click(),就少直接 render_click(view, ...)

直接触发事件当然更省事,但 element 这层会顺手帮我确认一件更接近现实的事:

这个事件,真的是从页面上那个元素发出来的吗?

这看着像细节,实际很值。很多"改了模板顺手把 phx-click 搞丢了"的事故,就是这么被拦下来的。

如果页面里有 LiveComponent,我现在也不太爱把它当"特殊物种"供起来。我的习惯反而更朴素:

  • 能通过父页面交互测到,就别急着单独拆
  • 只有组件状态和行为已经明显独立时,我才会考虑隔离去测

说到底,组件测试在 LiveView 里最有价值的,不是"我测了一个叫组件的东西",而是你能不能把那个可复用的小交互块,当成一个稳定边界看待。

2. handle_event/3 真正该测的,不是函数被调用,而是状态有没有流到页面上

我踩过一个很典型的坑:把 LiveView 测试写得像 service test。

比如点击保存,我只想确认保存函数被调了;点击删除,我只想确认上下文层收到了正确参数。这样不能说错,但总差一口气。

因为对 LiveView 来说,handle_event/3 最重要的,从来不是"这个回调进没进去",而是:

  • 页面有没有出现校验信息
  • 成功后有没有 patch / redirect
  • 列表有没有刷新
  • flash 有没有出来

举个更像真实项目的例子。比如新建项目页,我现在通常会把"无效输入"和"成功提交"放在一个连续场景里看:

elixir 复制代码
test "新建项目时,先看到校验,再看到成功跳转", %{conn: conn} do
  {:ok, view, _html} = live(conn, ~p"/projects/new")

  invalid_html =
    view
    |> form("#project-form", %{
      "project" => %{"name" => "", "description" => "写点东西"}
    })
    |> render_change()

  assert invalid_html =~ "不能为空"

  view
  |> form("#project-form", %{
    "project" => %{"name" => "LiveView 实验室", "description" => "终于不手点了"}
  })
  |> render_submit()

  assert_patch(view, ~p"/projects")
  assert render(view) =~ "项目创建成功"
  assert render(view) =~ "LiveView 实验室"
end

我喜欢这种写法,是因为它不把一次提交切得太碎。

真实用户不会先执行"一个校验函数测试",再执行"一个保存函数测试",最后再执行"一个跳转测试"。用户只会很自然地经历一整串反馈。

LiveView 测试最香的地方,其实也在这里:你可以用相对低成本的方式,把这整串反馈串起来。

这点跟 SPA 挺不一样。

以前测前端交互,我经常要操心:

  • 表单状态是不是还没同步
  • DOM 更新是不是还要等一拍
  • 某个 hook/mock 是不是没配好

LiveView 这边也有异步,但它的异步更像"服务端事件处理完,页面该怎么变",思路明显更收敛。

3. PubSub 这块最容易让我露怯,因为它一上来就不是"一个人点页面"的故事了

如果说表单和点击事件还算普通,那 PubSub 就很有 LiveView 自己的性格。

我第一次写聊天室的时候,页面本地跑得特别顺。开两个标签页,发消息、收消息、在线状态变化,都挺像那么回事。然后我心里就开始犯懒:

"这都能跑了,广播还测啥。"

后来事实证明,这句话非常适合写进"回旋镖语录"。

因为 PubSub 场景最容易出现一种假象:

  • 单页交互正常
  • 发送动作正常
  • 但另一个订阅方没有更新,或者更新时机不对

这类问题,手点是能测,但特别靠运气。你一边切窗口一边观察,有时根本说不清到底是功能稳,还是你这次点得巧。

我后来更偏爱两种测法。

第一种,是从领域动作出发,让广播自然发生:

elixir 复制代码
test "收到新消息后,页面会出现最新内容", %{conn: conn, user: user, room: room} do
  {:ok, view, _html} = live(conn, ~p"/rooms/#{room.id}")

  {:ok, _message} =
    Chat.create_message(%{
      room_id: room.id,
      user_id: user.id,
      body: "有人在吗?"
    })

  assert render(view) =~ "有人在吗?"
end

这类测试我挺喜欢,因为它验证的不是"我手动广播了一条消息,所以页面响应了",而是:

业务动作发生了,页面作为订阅者有没有跟上。

第二种,是更直接一点,专门测页面收到消息后的反应:

elixir 复制代码
test "LiveView 收到进程消息后会刷新界面", %{conn: conn} do
  {:ok, view, _html} = live(conn, ~p"/thermo")

  send(view.pid, {:set_temp, 50})

  assert render(view) =~ "The temperature is: 50"
end

这种测法也值,尤其适合你已经确认广播链路没问题,现在只想看 handle_info/2 有没有把页面更新对。

但我后来会提醒自己一件事:

如果一个功能的价值本来就在"多用户同步",那最好别只测到 handle_info/2 为止。

不然很容易出现一种尴尬局面:你的页面处理消息没毛病,但消息压根没按真实业务路径发出来。

4. E2E 我后来不再问"要不要上",而是问"哪些东西非它不可"

我以前对端到端测试有两个极端。

第一个极端是完全不上,觉得 LiveViewTest 都这么能打了,浏览器层是不是可以省。

第二个极端是啥都想上 E2E,仿佛只要真浏览器一跑,世界就和平了。

后来我发现,这俩思路都容易把自己搞累。

4.1 LiveViewTest 很强,但它不是浏览器本人

有些东西,LiveViewTest 测起来又快又稳:

  • 页面挂载
  • 按钮点击
  • 表单 change / submit
  • patch、redirect、flash
  • 组件渲染和局部更新

但有些场景,它就是不如真浏览器来得直接:

  • JS hooks
  • 文件上传的整条交互链
  • 焦点、快捷键、拖拽这类偏浏览器行为
  • 登录后跨页面流转
  • WebSocket 断开重连时的真实体验

这时候再假装"全都能靠 LiveViewTest 解决",多少有点嘴硬。

4.2 我现在对 Wallaby 和 Playwright 的态度也没那么纠结了

如果非要我说得主观一点,我现在的判断是这样的。

Wallaby 的优点很明确:

  • 跟 Elixir 测试栈贴得近
  • 可以继续待在 ExUnit 这套语境里
  • 对"不想再引入一套 Node 测试工具链"的团队挺友好

但它也有个很现实的脾气:LiveView + 浏览器 + 数据库 sandbox 这一套接起来,不算轻。

尤其是你开始处理测试环境里的 endpoint、sandbox、LiveView 连接信息时,那个配置味儿会明显变重。

Playwright 则是另一种路线。

我喜欢它的点,恰恰不在"它不是 Elixir 的",而在"它就是个成熟的浏览器测试选手":

  • 多浏览器支持直接
  • 并行、重试、报告、trace 这些能力现成
  • 查 flaky 问题时,比很多轻量方案更有抓手

像这种流程,我就很愿意交给 Playwright:

ts 复制代码
import { test, expect } from "@playwright/test";

test("user can create a project from the browser", async ({ page }) => {
  await page.goto("/projects/new");
  await page.getByLabel("名称").fill("LiveView 实验室");
  await page.getByRole("button", { name: "保存" }).click();

  await expect(page).toHaveURL("/projects");
  await expect(page.getByText("项目创建成功")).toBeVisible();
  await expect(page.getByText("LiveView 实验室")).toBeVisible();
});

所以我后来基本不拿它们对立看了。

我的真实用法更像是:

  • LiveViewTest 做主力
  • Playwright 补少量关键路径
  • Wallaby 适合想把测试栈尽量留在 Elixir 里的场景

说白了,端到端测试不是为了证明你有多专业,而是为了兜住那些"离开测试进程模拟,就开始有浏览器味儿"的部分。

5. 我最后给自己定了一套很朴素的分层,不豪华,但挺管用

我现在做 LiveView 项目,测试大概会按这四层想:

5.1 业务规则,先在上下文层测

像权限、校验、状态流转、资源归属这种东西,我不想全压在 LiveView 里测。因为这些规则本来就不该只属于页面。

5.2 页面交互,优先交给 LiveViewTest

这是主战场。

我最关心的是:

  • 能不能 mount
  • 页面初始状态对不对
  • 点完、填完、提完之后,页面变没变

5.3 广播和多用户同步,要专门留测试位

因为这是 LiveView 最容易"本地看着行,改完就翻车"的地方。

5.4 关键闭环,再让 E2E 兜底

不是全站铺满,也不是一条没有。就挑那些你一旦坏了,用户会第一时间骂人的流程:

  • 登录
  • 创建核心资源
  • 上传
  • 支付前后页
  • 带 JS hook 的关键交互

这套分层对我最大的帮助,不是"覆盖率看起来更体面",而是让我每次写测试时,不再陷入那种经典自问:

"这事我到底该测在哪一层?"

一旦这个问题没那么纠结,人就轻松很多。

6. 说到底,LiveView 测试真正值钱的,不是省事,而是它更接近用户动作

我现在回头看,LiveView 测试最让我舒服的一点,其实不是"代码更少",也不是"语法更顺"。

而是它经常能让我把注意力放回一个很正常的问题:

用户这么点一下、填一下、切一下页面,东西到底会不会按预期工作?

这件事在很多前端框架里当然也能测,只是代价和心智路径经常更绕一点。LiveView 在这里的优势,不是神奇,而是直接。

当然,直接不代表可以偷懒。

我自己踩下来,最容易把测试写歪的还是这几种心态:

  • 只想证明内部逻辑对,不想确认页面有没有真的变
  • 只测单用户 happy path,不测广播和同步
  • 觉得 LiveViewTest 已经够强,就把浏览器层一刀砍了

这些想法我都动过。也都被项目教育过。

所以如果现在让我用一句话概括 LiveView 测试,我大概会这么说:

别把它写成"后端函数测试外面套一层壳",也别把它全扔给 E2E。最顺手的方式,还是拿它去验证一段段真实交互。

这篇算是我在 LiveView 系列里,对"工程化"这块一个很实际的感受。页面写得再顺,聊天室跑得再欢,不把测试补起来,后面改需求的时候,心里还是虚。

如果你也刚好处在"页面都能点,但每次改完都想祈祷一下"的阶段,那 LiveView 这套测试工具,真的值得早点认真用起来。不是为了追求仪式感,单纯是为了少给未来的自己挖坑。

相关推荐
蜡台1 小时前
uniapp vue2 转 vue3
前端·javascript·uni-app·vue3·vue2
Web4Browser1 小时前
Headless 浏览器自动化适合哪些任务?巡检、截图和提交动作要分级
运维·前端·自动化
用户059540174461 小时前
把前端内存泄漏排查从2小时压到5分钟,我们把它集成进了CI
前端·css
申君健24864182772022 小时前
Vapor 核心数据结构:Block原理
前端
Csvn2 小时前
工程化专题 | 改完组件保存,浏览器毫无反应,重启 dev server 又好了——气不气?
前端
我有满天星辰3 小时前
Vue 指令完全指南:从 Vue 2 到 Vue 3 的演进与实战
前端·javascript·vue.js
光影少年3 小时前
RN 渲染原理:不依赖WebView,原生控件渲染
前端·react native·react.js·前端框架
小白学过的代码3 小时前
# flv.js / mpegts.js 播不了国标摄像头(H.265 + G.711A)?纯前端 Jessibuca 无后端方案(附完整代码)
前端·javascript·h.265
console.log('npc')3 小时前
AI Agent 前端流式渲染核心技术文档(SSE \+ WebSocket \+ 打字机渲染)
前端·人工智能·websocket