Subscribe to access all episodes. View plans →

#136: ExUnit.Callbacks

Published September 7, 2021

ExUnit.Callbacks

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 ExUnitsetup 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.

© 2024 HEXMONSTER LLC