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

📅 2026/7/12 11:00:06
LiveView 怎么测:从组件测试到端到端,我终于不再只敢手点页面了
前言我最早写 LiveView 的时候测试意识其实很飘。原因也不复杂。一方面LiveView 给人的第一印象太容易让人放松警惕了。页面交互写在服务端状态不用前后端来回对齐很多改动我一边写一边点浏览器里看着都挺正常。那种感觉特别像“这玩意儿都已经省了我半个前端了总不至于测试还跟以前一样折磨人吧。”另一方面我之前被 SPA 测试教育过。不是说 React、Vue 不能测而是它们很多时候会把你拖进一个很熟悉的泥潭组件能不能挂起来是一回事状态管理要不要 mock 是一回事路由要不要补上下文是一回事接口、定时器、浏览器行为又是另外几回事测到后面经常有一种说不清的疲惫我到底是在验证功能还是在给测试环境补基础设施LiveView 在这点上确实讨喜很多。它更像是让你直接对着“用户看到的页面”和“用户触发的事件”下手心智负担比典型 SPA 测试小不少。但我后面才反应过来LiveView 测试不是没有难点它只是把难点换成了另一种风格你测的是服务端持有状态的页面你碰到的是进程消息、广播、重连、patch你要防的不是前端状态漂移而是“这个页面明明能开结果一交互就露馅”所以我现在对 LiveView 测试的理解很简单它不是更低级也不是更高级它只是更适合拿“用户动作”当测试单位。这个视角一换很多事情就顺了。1. 我后来最依赖的不是测内部实现而是mount - render - element - assert我一开始写测试最大的问题不是不会用LiveViewTest而是总想偷看内部。比如想直接断言 assign想跳过 DOM 直接打某个函数想尽快证明“代码逻辑没问题”。这种冲动很正常毕竟这样写起来快。但 LiveView 这东西一旦太早绕开页面你很容易测着测着就偏了。因为用户又不会盯着你的 socket assign 过日子。用户只会干三件事打开页面点东西、填东西看页面有没有按预期变化所以我后来越来越喜欢一条很土的路径mount - render - element - assert比如一个最普通的计数器页面我现在更愿意这么测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 有没有出来举个更像真实项目的例子。比如新建项目页我现在通常会把“无效输入”和“成功提交”放在一个连续场景里看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 场景最容易出现一种假象单页交互正常发送动作正常但另一个订阅方没有更新或者更新时机不对这类问题手点是能测但特别靠运气。你一边切窗口一边观察有时根本说不清到底是功能稳还是你这次点得巧。我后来更偏爱两种测法。第一种是从领域动作出发让广播自然发生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这类测试我挺喜欢因为它验证的不是“我手动广播了一条消息所以页面响应了”而是业务动作发生了页面作为订阅者有没有跟上。第二种是更直接一点专门测页面收到消息后的反应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.1LiveViewTest很强但它不是浏览器本人有些东西LiveViewTest测起来又快又稳页面挂载按钮点击表单 change / submitpatch、redirect、flash组件渲染和局部更新但有些场景它就是不如真浏览器来得直接JS hooks文件上传的整条交互链焦点、快捷键、拖拽这类偏浏览器行为登录后跨页面流转WebSocket 断开重连时的真实体验这时候再假装“全都能靠LiveViewTest解决”多少有点嘴硬。4.2 我现在对 Wallaby 和 Playwright 的态度也没那么纠结了如果非要我说得主观一点我现在的判断是这样的。Wallaby的优点很明确跟 Elixir 测试栈贴得近可以继续待在 ExUnit 这套语境里对“不想再引入一套 Node 测试工具链”的团队挺友好但它也有个很现实的脾气LiveView 浏览器 数据库 sandbox 这一套接起来不算轻。尤其是你开始处理测试环境里的 endpoint、sandbox、LiveView 连接信息时那个配置味儿会明显变重。Playwright则是另一种路线。我喜欢它的点恰恰不在“它不是 Elixir 的”而在“它就是个成熟的浏览器测试选手”多浏览器支持直接并行、重试、报告、trace 这些能力现成查 flaky 问题时比很多轻量方案更有抓手像这种流程我就很愿意交给 Playwrightimport{test,expect}fromplaywright/test;test(user can create a project from the browser,async({page}){awaitpage.goto(/projects/new);awaitpage.getByLabel(名称).fill(LiveView 实验室);awaitpage.getByRole(button,{name:保存}).click();awaitexpect(page).toHaveURL(/projects);awaitexpect(page.getByText(项目创建成功)).toBeVisible();awaitexpect(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 这套测试工具真的值得早点认真用起来。不是为了追求仪式感单纯是为了少给未来的自己挖坑。