Testing Async Assigns
LiveView allows us to assign things asynchronously. Let’s see how we can test that.
If you open the app, you’ll see MetricsLive mounted under /metrics. It shows a loading indicator first, then metric data.
In mount/3:
def mount(params, _, socket) do
{:ok, socket |> assign_async(:metric, fn -> fetch_metric(params) end)}
end
And in fetch_metric/1:
defp fetch_metric(params) do
force_fail = Map.get(params, "test_force_failure", false)
if force_fail do
{:error, "Loading error"}
else
Process.sleep(1500)
{:ok, %{metric: %{value: 123, name: "User Visits"}}}
end
end
This sleeps to emulate async work and then returns metric data.
Render behavior
render/1 typically handles three states:
@metric.loading-> loading skeleton (data-role="loading")@metric.ok?-> success state (data-role="success")- else -> failure state (
data-role="failure")
Testing loading indicator
test "renders loading indicator when loading", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/metrics")
assert html =~ "data-role=\"loading\""
end
This is a by-proxy assertion because we don’t have obvious user text in the loading skeleton.
Testing async data with sleep (works, but not ideal)
test "renders metric after loading (sleep)", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/metrics")
Process.sleep(2000)
assert has_element?(view, "[data-role=success]", "User Visits")
end
Why this is weak:
Process.sleep/1in tests is usually a smell.- It always waits the full time, slowing test suites.
Better approach: render_async/2
test "renders metric after loading", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/metrics")
assert render_async(view, 2000) =~ "User Visits"
end
render_async/2 waits up to timeout and returns as soon as async work completes.
Remove mystery timeout values
One way is to expose timeout in your LiveView and use it in test:
test "renders metric after loading", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/metrics")
timeout = RangerWeb.MetricsLive.metric_timeout()
assert render_async(view, timeout + 100) =~ "User Visits"
end
Another way is to read application config:
timeout = Application.get_env(:ranger, RangerWeb.MetricsLive)[:timeout]
assert render_async(view, timeout + 100) =~ "User Visits"
Testing failure path
One option is forcing failure via query params:
test "renders failure case upon failure", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/metrics?test_force_failure=true")
assert render_async(view) =~ "Failed to load"
end
This is pragmatic but exposes test-oriented behavior.
Alternatives:
- Use app config toggles in tests (
async: falsewhen mutating config) - Mock external metric-fetching module (for example, Mox/Mock)