CRUD
First context test
Read time: 8 minutes
This class is a direct continuation of the previous class
git clone https://github.com/adopt-liveview/lineup.git --branch saving-data-done.
I might be biased since I like Elixir and Phoenix so much to say this but I can tell you that this is the stack I'm most comfortable writing tests for frontend code that I've ever been. And there's a good reason for it: HEEx code is just a as functional as a frontend can get! You pass arguments over assigns and you get a generated HTML code.
Of course right now you must be cursing at me for claiming since there are obvious side effects within HEEx code such as navigation, form update/submit events and even JS calls with JS commands. But give me a few lessons to show you that it will all make sense in the end.
#Our tools
Elixir ships with a test runner and assertion library called ExUnit, you don't need to install anything. In fact, we already have some tests built in by Phoenix. Also, since we never paid attention to it, we actually broke it many lessons ago!
Inside your LiveView project run mix test:
....
1) test GET / (LineupWeb.PageControllerTest)
test/lineup_web/controllers/page_controller_test.exs:4
Assertion with =~ failed
code: assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
left: "<!DOCTYPE html>A BUNCH OF HTML CODE!</html>"
right: "Peace of mind from prototype to production"
stacktrace:
test/lineup_web/controllers/page_controller_test.exs:6: (test)
Finished in 0.04 seconds (0.01s async, 0.03s sync)
5 tests, 1 failure
Don't worry if you get some warnings, let's focus on the error. In a vibrant red color you should see that our test rendered a bunch of HTML code but it expected to at least have "Peace of mind from prototype to production" written inside it. To digest what a failing test means:
1) test [Here's the name of the test case] ([Here's the name of the test module])
[here's the relative path to the test file]:[here's the line number where this test case is written]
Assertion with =~ failed <- [some information about why it failed, it says a `=~` operator did not pass]
code: [test code that failed]
left: [what's written inside the left hand side of the =~ operator]
right: [what's written inside the right hand side of the =~ operator]
stacktrace:
[relative path]:[line number]: (test) <- [where exactly your test failed inside this test case]
Knowing how to read those will make your life easier. The error above is a simple case "text-based match". When used like this body =~ part we expected part to be contained inside body. In this case html_response(conn, 200) =~ "Peace of mind from prototype to production" we expected the result HTML to contain that phrase. Let's head out to test/lineup_web/controllers/page_controller_test.exs:6, focus solely on the test case:
test "GET /", %{conn: conn} do
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
end
As you can see we have tons of new things here to understand. For now lets focus on fixing this test. By starting our Super Store project, we can spot that the root page (/) has a "Listing Products" heading. Let's update our test and run mix test again.
test "GET /", %{conn: conn} do
conn = get(conn, ~p"/")
- assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
+ assert html_response(conn, 200) =~ "Listing Tickets"
end
$ mix test
Running ExUnit with seed: 610698, max_cases: 28
.....
Finished in 0.08 seconds (0.03s async, 0.05s sync)
5 tests, 0 failures
And that's it, we have our test suite passing!
#Our context module test
Since we already have some new code for our ticket management system we might as well starting building our test cases. Lets create our context module test file. Create: test/lineup/queue_test.exs (you might need to create the lineup folder inside test).
defmodule Lineup.QueueTest do
use Lineup.DataCase
alias Lineup.Queue
describe "tickets" do
alias Lineup.Queue.Ticket
import Lineup.QueueFixtures
test "create_ticket/1 with valid data creates a ticket" do
valid_attrs = %{called_at: ~U[2026-04-27 16:00:00Z]}
assert {:ok, %Ticket{} = ticket} = Queue.create_ticket(valid_attrs)
assert ticket.called_at == ~U[2026-04-27 16:00:00Z]
end
test "change_ticket/1 returns a ticket changeset" do
ticket = ticket_fixture()
assert %Ecto.Changeset{} = Queue.change_ticket(ticket)
end
end
end
For that to work we will also need a helper so create test/support/fixtures/queue_fixtures.ex (you might need to create the fixtures folder):
defmodule Lineup.QueueFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Lineup.Queue` context.
"""
@doc """
Generate a ticket.
"""
def ticket_fixture(attrs \\ %{}) do
{:ok, ticket} =
attrs
|> Enum.into(%{
called_at: ~U[2026-04-27 16:00:00Z]
})
|> Lineup.Queue.create_ticket()
ticket
end
end
Now mix test can be used to run that specific test file; you have to pass its relative path. Try it:
$ mix test test/lineup/queue_test.exs
Running ExUnit with seed: 469161, max_cases: 28
..
Finished in 0.04 seconds (0.00s async, 0.04s sync)
2 tests, 0 failures
Let's talk about what we just wrote! First of all in Elixir we try to have some parity between where code lives and where their respective test case lives. In our case lib/lineup/queue.ex has a test file in test/lineup/queue_test.exs. Also as you can see we append _test to file and module names so Lineup.Queue is tested by Lineup.QueueTest. Another difference is that for test files we use the .exs extension which is common to Elixir scripts whilst .ex is used for parts of projects.
Just like in LiveViews we use MyappWeb, :live_view we also have helpers to write tests. In our case, since we are working with regular data, we use Lineup.DataCase. That module is already written inside your project and just like LineupWeb it will import and alias useful things for your test case. You can open test/support/data_case.ex in case you're curious but unless you need to install something in there it's likely you won't need to bother with it at all.
You might have also noticed that we have describe "tickets" do ... end inside our test file. ExUnit's describe/2 works to group tests together not only in code but also when formatting them. Later on we will see how we can use them to setup common shared code alongside multiple individual test cases inside it. Also, it's worth mentioning that since we are testing the Lineup context module and context modules can have more than one ecto model, it's useful to have them separate each part of your business logic.
In our first test case "create_ticket/1 with valid data creates a ticket" we just verify in our unit tests what we already built before: passing some attributes to Queue.create_ticket/2 will generate a new ticket. As for our second test case "change_ticket/1 returns a ticket changeset" we ensure that passing a valid ticket will generate an %Ecto.Changeset{}.
At the very top of our change_ticket test case we use ticket_fixture/1 to quickly create a simple ticket that we can use in our test code. We imported that just a few lines before in import Lineup.QueueFixtures and that's basically what fixtures are meant to do.
With both tests set we can ensure that we will know beforehand if we ever break those contracts and we can check that our expected behaviors might have regressed or just changed somehow.
#Recap
In this lesson, we've taken our first steps into testing with Elixir and Phoenix:
- We discovered that Elixir comes with ExUnit, a built-in test runner and assertion library
-
We ran our first test suite using
mix testand encountered a failing test -
You can run a specific test file by passing its relative path such as
mix test test/inner/something_test.exs - We learned how to read and interpret test failure messages in ExUnit
- Elixir's built-in test runner is called ExUnit
-
In Elixir we have a convention of placing test files inside
testfolder with the same path as it would have been inlibfolder (lib/inner/something.exhas a test intest/inner/something_test.exs), with test modules havingSomethingTestnaming and.exsextension -
We use
describeblocks to organize groups of tests - We use fixtures to quickly create some test data such as storing things in the database
-
Phoenix will come with
Myapp.DataCaseto help with tests related to data (usually context modules).
Feedback
Got any feedback about this page? Let us know!