Subscribe to access all episodes. View plans →

#92: Introduction to Testing

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.

© 2024 HEXMONSTER LLC