Published April 29, 2019
Testing is an essential part software development and Elixir comes with a great test framework called ExUnit. In this episode we’ll see how easy it is to get started writing tests in Elixir.
To start let’s create a new Elixir application named
$ mix new teacher ... Your Mix project was created successfully. You can use "mix" to compile it, test it, and more: cd teacher mix test Run "mix help" for more commands.
If we look at what’s printed, it tells us how we can compile our project and how we can run the tests. Let’s go ahead and run our tests. We’ll move into our
teacher directory and then to run our tests we’ll run
mix test - this will run all the tests in our project.
$ cd teacher/ $ mix test Compiling 1 file (.ex) Generated teacher app .. Finished in 0.03 seconds 1 doctest, 1 test, 0 failures Randomized with seed 920136
It looks like we have 1 doctest that ran and one 1 test, both passed. Let’s open our project and take a closer look at what was created for us. When we created our project a module named
teacher.ex was generated for us, let’s open that and inside it we have one function that was generated for us -
hello that returns an atom named
Above our function we see the
@doc module attribute which is used before a function to provide documentation for it. What’s great is that it’s also a test and if our function is updated to return something other than
:hello our function
@doc needs to be updated as well otherwise it will fail.
Let’s test that out. We’ll change our function
@doc to return something unexpected.
... @doc """ Hello world. ## Examples iex> Teacher.hello() :zworld """...
And if we go back to the command line and run
mix test again.
$ mix test Compiling 1 file (.ex) . 1) doctest Teacher.hello/0 (1) (TeacherTest) test/teacher_test.exs:3 Doctest failed code: Teacher.hello() === :zworld left: :world right: :zworld stacktrace: lib/teacher.ex:11: Teacher (module) Finished in 0.03 seconds 1 doctest, 1 test, 1 failure Randomized with seed 880562
We can see our doctest failed. This is a great freature of Elixir that ensures our documentation is always in sync with our code. Now let’s look at the other test that ran. We’ll go back to our editor and we see that there’s a
test folder, and if we open that we see there’s a file
teacher_test.exs, this is where the tests for our
teacher.ex module are.
When we run
mix test all the tests in files that end in
_test.exs will be run. So if you’re adding any new modules and tests, just be sure that they follow that convention. Let’s open our
teacher_test.exs and we see there’s a single test inside it “greets the world” that asserts that our
Teacher.hello function returns
test macro is imported by
use ExUnit.Case above.
ExUnit.Case provides helpers for defining tests. By default test are not run concurrently, but if we wanted to run our tests concurrently we just need to inlcude
async: true and our tests here will be run concurrently with tests in other modules. You shouldn’t use this option if your tests change any global state.
Now that we’ve seen the layout of our module and its tests, let’s update our application and write some tests for our changes. We’ll update our
teacher.ex module with a function that returns the even numbers from a list and to implement this function, let’s use Test-Driven Development or TDD. We’ll first write our tests and then implement the code to get them to pass.
Let’s start by using another macro
describe to group our tests. We’ll use
describe to organize tests. Let’s call the function to return our even numbers
evens/1. Then inside our
describe we can write our tests for how we expect our function to behave.
Let’s add a new tests to check that even numbers are returned. We’ll define our test and then inside it we’ll use
assert which will pass if our assertion is true. So let’s say that when we call our
Teacher.evens function with a list of numbers, we expect to get a list returned with only the even numbers included. Then let’s add another test specifying that “no odd numbers are returned” and instead of using
assert let’s use
refute which is a negative assertion - it expects the expression to be
Let’s call our function and assign the result the
evens variable. The let’s
refute that the odd number in this list
3 is not not included in our returned list.
defmodule TeacherTest do use ExUnit.Case doctest Teacher describe "Teacher.evens/1" do test "even numbers are returned" do assert Teacher.evens([1,2,3,4]) == [2,4] end test "no odd numbers are returned" do evens = Teacher.evens([2,3,4]) refute Enum.member?(evens, 3) end endend
Then we’ll go to the command line and run our tests.
$ mix test Compiling 1 file (.ex) 1) test Teacher.evens/1 even numbers are returned (TeacherTest) test/teacher_test.exs:6 ** (UndefinedFunctionError) function Teacher.evens/1 is undefined or private code: assert Teacher.evens([1,2,3,4]) == [2,4] stacktrace: (teacher) Teacher.evens([1, 2, 3, 4]) test/teacher_test.exs:7: (test) 2) test Teacher.evens/1 no odd numbers are returned (TeacherTest) test/teacher_test.exs:10 ** (UndefinedFunctionError) function Teacher.evens/1 is undefined or private code: evens = Teacher.evens([2,3,4]) stacktrace: (teacher) Teacher.evens([2, 3, 4]) test/teacher_test.exs:11: (test) Finished in 0.03 seconds 2 tests, 2 failures Randomized with seed 181519
Great - we see both of our tests failed, which is what we wanted. Now let’s create our
evens function to get them to pass.
Back in our
Teacher module let’s define a new function
evens that takes our list of numbers. Then inside the function we’ll use
Enum.fiter to grab all the even numbers out of the list. And to check if the number is even, we’ll use
Integer.is_even and because
Integer.is_even is a macro, we’ll need to add
require Integer above in order to use it.
...require Integerdef evens(list) do Enum.filter(list, &Integer.is_even/1)end...
Now let’s go back to the command line and run our tests again
$ mix test Compiling 1 file (.ex) .. Finished in 0.03 seconds 2 tests, 0 failures Randomized with seed 871514
And now they all pass.
Now let’s make a change. Instead of returning the even numbers in the order of their original list, let’s sort them, so that the smallest numbers come first.
Just like before, let’s let our tests drive our development.
We’ll open our
teacher_test.exs module and let’s write a test to check that our return is sorted.
...test "the return is sorted" do assert Teacher.evens([4,3,2,1]) == [2,4]end...
Then let’s run our test to make see if it fails.
And instead of running all of our tests, let’s try only running our new test.
We can do that with mix test then the path to the test file we want to run followed by a colon and then the line number of the test we want to run.
So here the test is on line 15 so we’ll write:
mix test test/teacher_test.exs:15.
Then if we run the test:
$ mix test test/teacher_test.exs:15 Excluding tags: [:test] Including tags: [line: "15"] 1) test Teacher.evens/1 the return is sorted (TeacherTest) test/teacher_test.exs:15 Assertion with == failed code: assert Teacher.evens([4, 3, 2, 1]) == [2, 4] left: [4, 2] right: [2, 4] stacktrace: test/teacher_test.exs:16: (test) Finished in 0.03 seconds 3 tests, 1 failure, 2 excluded Randomized with seed 490581
We see only the test that we specified was executed.
Now let’s make our test pass.
We’ll open up our
And after we filter for our even numbers, let’s add a call to
...def evens(list) do list |> Enum.filter(&Integer.is_even/1) |> Enum.sort()end...
Great now let’s go back to the command line and this time let’s run all our tests to make sure the changes to our function to break any existing tests.
$ mix test ... Finished in 0.03 seconds 3 tests, 0 failures Randomized with seed 85479
Perfect - all our tests passed.