Subscribe to access all episodes. View plans →
Published September 7, 2021
Follow along with the episode starter on GitHub
In this episode we’ll learn how to use the ExUnit.Callbacks
to define test fixtures and get our tests setup with the data they need. Specifically, we’ll use the setup
and setup_all
callbacks as well as the on_exit
function. If you’re new to testing with Elixir, check out episode 92 for an introduction to testing. We’ll use an Elixir Phoenix application that lists a few different albums.
When we created this application we used the Phoenix context generator to create a Recordings
context module. This is the recordings.ex
module that Phoenix created for us. It has functions to help us manage our albums. And along with this module, Phoenix created a corresponding test module in recordings_test.exs
- let’s open that. Here we have the tests that Phoenix created for the recordings.ex
context module.
The album_fixture
function here inserts a new album into the database and returns it. And we can see that most of these tests use it to create an album. Instead of calling album_fixture
in each of these tests, let’s update them to use the ExUnit
setup
callback to create an album for our tests to use. The setup
callback will run before each test.
Before we make any changes to our tests here, let’s run them to ensure they pass. We’ll go to the command line and instead of running the whole test suite we’ll just run the recordings_test.exs
which we can do by running mix test
with the path to our test file, in this case, test/teacher/recordings/test.exs
. Great - all our tests pass.
$ mix test test/teacher/recordings_test.exs
........
Finished in 0.2 seconds
8 tests, 0 failures
Now that we know our tests are passing let’s add the setup
callback. We’ll go back to our tests and define our setup
block then inside we can handle whatever test setup we need to. In this case, we’ll create an album. Once we have our album, we need to make it available for our test. Now we can’t just call setup
like we are with the album_fixture
function. Instead, we need to add the album
to the test state and metadata that’s known as the context.
To do that we can return an OK tuple with the state we want to add, in this case, our album
. We can also just return a map with the data we want to include, this will update the test context with our album.
Then let’s go to a test where we need an album and now because our album is in our test context instead of calling album_fixture
we can pattern match on the context to get our album.
test/teacher/recordings_test.exs
...
setup do
{:ok, album} =
%{artist: "some artist", summary: "some summary", title: "some title", year: 1972}
|> Recordings.create_album()
%{album: album}
end
test "list_albums/0 returns all albums", %{album: album} do
assert Recordings.list_albums() == [album]
end
...
Let’s check that our updated test works. We’ll go to the command line and let’s run mix test
again, but let’s only run the test we updated. To do that we’ll append the line number of the test we want to run. In this case, our test is on line 30 so we’ll use that.
$ mix test test/teacher/recordings_test.exs:30
.
Finished in 0.1 seconds
8 tests, 0 failures, 7 excluded
Great - our test passed. It’s now using the album
created from the setup
callback. Now we can go back to our tests and update the other tests to use our setup
With that we can remove the album_fixture
since we won’t need it anymore. Let’s check that all our tests still pass. Great, all our tests are updated to use the setup
callback to create an album
.
$ mix test test/teacher/recordings_test.exs
Finished in 0.1 seconds
8 tests, 0 failures
Now one other way to create a setup is with a function. To do that with our existing setup
we’ll define a new private function named create_album
that will take the test context
which we can ignore here since we won’t need it. In our function, we can create an album and return it just like before.
Then we can invoke our create_album
function by calling setup
with a list of function names we want to call as atoms. This pattern is great for creating composable setups to use across your tests. Now we can remove our setup
because we no longer need it.
test/teacher/recordings_test.exs
...
setup [:create_album]
defp create_album(_context) do
{:ok, album} =
%{artist: "some artist", summary: "some summary", title: "some title", year: 1972}
|> Recordings.create_album()
%{album: album}
end
...
Now let’s take a quick look at the other setup callback setup_all
. The main difference between setup
and setup_all
is where setup
is run before each test, setup_all
is run before all tests. So let’s go ahead and add a setup_all
callback outside of our describe
. Then let’s have it just print a message and because we don’t want to modify the test context here, we’ll return an :ok
atom. One more difference between the setup_all
and setup
callbacks is that setup_all
is executed in a separate process than the tests, whereas setup
is executed in the same process as the tests.
Now another handy callback is on_exit
. This is useful when you need to tear down any setup you’ve done. We don’t need to do any teardown here, so let’s use on_exit
to print a message that our tests are done. We’ll call on_exit
and then give it a function that will print “All done”.
test/teacher/recordings_test.exs
...
setup_all do
IO.puts("Setting up for all tests")
on_exit(fn -> IO.puts("All done") end)
:ok
end
...
Alright, let’s go to the command line one last time and run our test file. Great - when we do our messages are printed and all our tests pass.
$ mix test test/teacher/recordings_test.exs
Setting up for all tests
........All done
Finished in 0.1 seconds
8 tests, 0 failures
Our recordings_test.exs
is updated to use ExUnit.Callbacks
.