Goal: have a personal blog, and try out another point in the 'modular app design with elixir' space. Designing OTP systems with elixir had some interesting ideas.
67 lines
2.1 KiB
Markdown
67 lines
2.1 KiB
Markdown
%{
|
|
title: "How We Test LiveView at Scale",
|
|
author: "Carlos Rivera",
|
|
tags: ~w(elixir liveview testing),
|
|
description: "Our testing strategy for 200+ LiveView modules"
|
|
}
|
|
---
|
|
With over 200 LiveView modules in our codebase, we needed a testing strategy
|
|
that was both fast and reliable. Here's what we landed on.
|
|
|
|
## The three-layer approach
|
|
|
|
We test LiveViews at three levels:
|
|
|
|
1. **Unit tests** for the assign logic — pure functions, no rendering
|
|
2. **Component tests** for individual function components using `render_component/2`
|
|
3. **Integration tests** for full page flows using `live/2`
|
|
|
|
The key insight is that most bugs live in the assign logic, not in the
|
|
templates. By extracting assigns into pure functions, we can test the
|
|
interesting bits without mounting a LiveView at all.
|
|
|
|
```elixir
|
|
defmodule MyAppWeb.DashboardLive do
|
|
use MyAppWeb, :live_view
|
|
|
|
# Pure function — easy to test
|
|
def compute_metrics(raw_data, date_range) do
|
|
raw_data
|
|
|> Enum.filter(&in_range?(&1, date_range))
|
|
|> Enum.group_by(& &1.category)
|
|
|> Enum.map(fn {cat, items} ->
|
|
%{category: cat, count: length(items), total: Enum.sum_by(items, & &1.value)}
|
|
end)
|
|
end
|
|
end
|
|
|
|
# In the test file
|
|
test "compute_metrics groups and sums correctly" do
|
|
data = [
|
|
%{category: "sales", value: 100, date: ~D[2026-03-01]},
|
|
%{category: "sales", value: 200, date: ~D[2026-03-02]},
|
|
%{category: "support", value: 50, date: ~D[2026-03-01]}
|
|
]
|
|
|
|
result = DashboardLive.compute_metrics(data, {~D[2026-03-01], ~D[2026-03-31]})
|
|
|
|
assert [
|
|
%{category: "sales", count: 2, total: 300},
|
|
%{category: "support", count: 1, total: 50}
|
|
] = Enum.sort_by(result, & &1.category)
|
|
end
|
|
```
|
|
|
|
## Speed matters
|
|
|
|
Our full test suite runs in under 90 seconds on CI. The secret is
|
|
`async: true` everywhere and avoiding database writes in unit tests.
|
|
We use `Mox` for external service boundaries and `Ecto.Adapters.SQL.Sandbox`
|
|
only for integration tests.
|
|
|
|
## What we'd do differently
|
|
|
|
If starting over, we'd adopt property-based testing with `StreamData` earlier.
|
|
Several production bugs would have been caught by generating edge-case assigns
|
|
rather than hand-writing examples.
|