- Published on
Heyya Snap It Like A Polaroid
Batteries Included has just released a new open-source library for testing
Phoenix components. The library, called
Heyya, makes it easy to create
and verify snapshots of your Phoenix components, helping to ensure their
correctness and stability. It’s a combination of Phoenix’s
Phoenix.LiveViewTest
and Snapshy
that’s been inspired by
react snapshot testing, including
Jest
Snapshot Testing
Snapshot testing is a powerful tool that can help to ensure the correctness and stability of your Phoenix LiveView components. It creates a “snapshot” of the component’s rendered output and saves it in a text file. The snapshot is compared to the latest markup during subsequent tests to ensure it has stayed the same.
Stateless
Snapshot testing can benefit functional components in Phoenix LiveView because these components are typically e straightforward to test than stateful components. Functional components are pure functions without any internal state or side effects. This functional purity means they always produce the same output for a given set of inputs. With snapshot testing, you can create a snapshot of the rendered output of a functional component and then use that snapshot to verify that the component continues to produce the same output for the same inputs. This ensures that the component is correct and stable and can help to catch any regressions or other issues that might not be immediately apparent when manually testing the component.
Initial Example
For this example, let us assume that we are creating a new app and want a unified header text. So we might make a header module that looks something like this:
defmodule Header do
use Phoenix.Component
slot :inner_block, required: true
def simple(assigns) do
~H"""
<h1>
<%= render_slot(@inner_block) %>
</h1>
"""
end
end
We can easily create a test that checks to make sure that Header.simple/1
works with the following code:
defmodule HeaderTest
use Heyya
component_snapshot_test "Header test" do
# The assigns map contains values that will be accessible in the H sigil
assigns: %{}
~H"""
<Header.simple>Testing</Header.simple>
"""
end
end
That code is enough to ensure that passing the slot works and produces the
expected result. For so little work with easy maintenance, snapshot testing can
have a great payoff. Under the hood, the return value from this “Header test”
block is getting rendered to a string. Then Snapshy will compare that string
value to the stored in test/__snapshots__/**/*.snap
. If this is the first test
run, Snapshy will store the output in files that need to be added to your source
control.
Code Changes Always
Let us explore that easy maintenance with an example of how snapshot testing, which might appear brittle and hard to use in a changing code base, still allows for a fast development pace.
Later, for example, we might change the component to look like the following:
attr :class, :any, default: "text-3xl font-bold tracking-tight text-gray-900"
slot :inner_block, required: true
def simple(assigns) do
~H"""
<h1 class={@class}>
<%= render_slot(@inner_block) %>
</h1>
"""
end
Notice that we added a default css class that will make the text look nicer. However, it will also make the previous tests fail with an error. We’ll get an error like this:
Generated common_testing app
1) test Header test (CommonTesting.ComponentSnapshotTestTest)
test/common_testing/component_snapshot_test.exs:4
Received value does not match stored snapshot. (__snapshots__/common_testing/component_snapshot_test/test__header_test.snap)
code: "Snapshot == Received"
left: "<h1>Testing</h1>"
right: "<h1 class=\"text-3xl font-bold tracking-tight text-gray-900 \">\n Testing\n</h1>"
stacktrace:
(snapshy 0.3.0) lib/snapshy.ex:147: Snapshy.raise_error/3
(snapshy 0.3.0) lib/snapshy.ex:118: Snapshy.match/2
test/common_testing/component_snapshot_test.exs:4: (test)
Finished in 0.04 seconds (0.00s async, 0.04s sync)
1 test, 1 failure
Rather than writing new assertions that the rendered code contains “text-3xl font-bold tracking-tight text-gray-900”, we can re-run the failing test with a particular environment variable instead.
$ SNAPSHY_OVERRIDE=true mix test
Compiling 1 file (.ex)
S.
Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 test, 0 failures
Randomized with seed 527608
That environment variable SNAPSHY_OVERRIDE=true
will reset the snapshot, so
all future tests will assert that the component yields the new expected output.
It’s also straightforward to add a test to show that setting the class attribute
has the desired effect.
defmodule HeaderTest do
use Heyya
component_snapshot_test "Header test" do
assigns: %{}
~H"""
<Header.simple>Testing</Header.simple>
"""
end
component_snapshot_test "Header test explicit class" do
assigns: %{custom_class: "my-class"}
~H"""
<Header.simple class={@custom_class}>Testing with static</Header.simple>
"""
end
end
Run mix test
, then verify that the new .snap
file contains the expected
‘my-class’ and that’s it. Creating a test that demonstrates the working parts of
the component is as easy as just using the feature. It’s so easy that you’ll
have no excuse.
Where
Check out the documentation website at https://hexdocs.pm/heyya/readme.html and the code on GitHub at https://github.com/batteries-included/heyya.
Happy testing!