Subscribe to access all episodes. View plans →
Published April 29, 2019
Elixir 1.7
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 teacher
.
$ 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 :world
.
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.
lib/teacher.ex
...
@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 :world
.
This 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 false
or nil
.
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.
test/teacher_test.exs
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
end
end
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.
lib/teacher.ex
...
require Integer
def 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/teacher_test.exs
...
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 teacher.ex
module.
And after we filter for our even numbers, let’s add a call to Enum.sort
lib/teacher.ex
...
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.