Introduction

Testing is one of the most important tools in a software developer's arsenal; it's also one of the most underused, historically (although things seem to be getting a lot better on that front lately).

Today, we're going to cover:

  • What is testing, and what is good for.
  • ExUnit, Elixir's built-in Unit Testing framework.
  • Building an example module via TDD.
  • An awesome online tool and community called Exercism.io, which hails from the Ruby community originally.
  • A great feature Elixir provides called doctests

What is Testing

People often see testing's value in avoiding regressions, and that's a fantastic feature it provides. Tests allow you to refactor your code without concern that you've accidentally changed behaviour in some subtle way.

I see an even greater value in testing as the way to plan the code you're going to write. Testing is how I think now, and I find it very difficult to build a system without doing it by writing and satisfying a series of unit tests. It also provides very valuable feedback that helps you know when a module in your system is getting too complex - if the tests are getting hard to write, the odds are your code is doing too much.

The time between when I learned how to test in Erlang and I'd had a working chat server was just a few hours.

Unit Testing - system under test

Unit testing is a means of testing just a single 'unit' in your system. This can be contrasted with an acceptance test, which tests the behaviour of a system as a whole, and verifies that it satisfies the overarching requirements.

Unit tests, on the other hand, are focused with a single portion of the system, and should fake, or 'mock' any collaborators out, so that a single portion of the system can be verified on its own.

ExUnit

Elixir comes with a built in tool for writing unit tests, called ExUnit. An ExUnit test case is just a module that uses ExUnit.Case. We'll build a test case in a little bit, but for now I'll just tell you a few more things about ExUnit.

Defining Tests

ExUnit.Case will run all functions whose names start with test that have arity 1 - that means, they only take a single argument.

For instance, you could define a function like this:

def test_one_is_one(_) do
  assert 1 == 1
end

However, ExUnit provides a test macro that allows you to write your tests a bit easier on the eyes:

test "one is one" do
  assert 1 == 1
end

Assertions

We just saw assert, which is a macro provided by ExUnit to describe the intended behaviour of your system.

For instance, if you have the following test case:

test "one is two" do
  assert 1 == 2
end

When you run the suite, this test will fail with "Expected 1 to be (==) 2". The inverse of assert is refute. These two macros provide most of what you'll need to write your tests. There are a few others provided, and you can check them out in ExUnit's Assertions documentation

Examples

To get comfortable testing in Elixir, we're going to create a module of our own using Test-Driven-Development, or TDD. If you're unfamiliar with this concept, it's basically the idea that you write your tests first, then write just enough code to make them pass, and no more. That is, you let the tests "drive" the development of your codebase.

We're going to test-drive a module called Schizo. It's going to provide two functions: uppercase and unvowel. These functions will uppercase every other word, and remove the vowels from every other word, respectively.

To get started, we'll use mix to create a new app:

mix new schizo

Go ahead and cd into the schizo directory.

Since we'll be using TDD, let's go ahead and modify the test file that mix created for us, and define some behaviour for our first function, uppercase.

defmodule SchizoTest do
  use ExUnit.Case

  test "uppercase doesn't change the first word" do
    assert(Schizo.uppercase("foo") == "foo")
  end

  test "uppercase converts the second word to uppercase" do
    assert(Schizo.uppercase("foo bar") == "foo BAR")
  end

  test "uppercase converts every other word to uppercase" do
    assert(Schizo.uppercase("foo bar baz whee") == "foo BAR baz WHEE")
  end
end

Now let's just start implementing, making tests pass one by one as we go.

... hack session ...

Now that we've implemented uppercase, let's implement unvowel. First, we write some tests:

test "unvowel doesn't change the first word" do
  assert(Schizo.unvowel("foo") == "foo")
end

test "unvowel removes the second word's vowels" do
  assert(Schizo.unvowel("foo bar") == "foo br")
end

test "unvowel removes every other word's vowels" do
  assert(Schizo.unvowel("foo bar baz whee") == "foo br baz wh")
end

Once again, we run the tests and start implementing, step-by-step, until they pass.

... hack session ...

Now, TDD consists of "red, green, refactor." So far, we've just done "red, green." There's a lot of duplication here, and removing it will teach us some fun stuff about elixir, so let's go ahead and refactor this until we're happy with it. The whole point of the tests is that we can do this without fear.

... hack session ...

Now we're left with something quite nice looking, and it's very easy to extend it with more functions along the same lines. We just need to define the transformation functions, and we're basically done.

Exercism.io

Katrina Owen built a fantastic tool/community known as exercism.io The goal is to get and give peer review while doing basic code katas, and to try to converge on the 'ideal' solution to various given problems in different languages. They have an Elixir track, and it was the first Elixir code I ever wrote. I'd definitely suggest people go over there and get an account - it will help you hone your chops, with great feedback from people who care.

Doctests

Elixir also ships with support for something called doctests. Basically, if you place an example iex session in your module or function documentation, you can easily verify its behaviour by specifying a doctest in your test case.

This was cribbed from Python, to my knowledge, but coming from Ruby I never have had a chance to play with it. It's amazing. Let's go ahead and add a doctest to the Schizo module so you can see how it works:

@moduledoc """
This is a module that provides odd behaviour for transforming every other word in a string.

Here are some examples:

iex> Schizo.uppercase("this is an example")
"this IS an EXAMPLE"

iex> Schizo.unvowel("this is an example")
"this s an xmpl"
"""

To add these doctests to your test suite, open up the SchizoTest module and just add the following line:

doctest Schizo

Then, run the tests again, and two new test cases have been added. Pretty cool, huh? Gone are the days of documentation that is subtly incorrect!

Summary

That wraps up this episode. Today, we learned how to write unit tests, TDD a module from the ground up, and explored DocTests. Armed with the ability to TDD your code, you should be able to level up in Elixir substantially faster from here on out. See you soon!

Links