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.
Elixir comes with a built in tool for writing unit tests, called
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.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
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
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
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
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
Since we'll be using TDD, let's go ahead and modify the test file that
created for us, and define some behaviour for our first function,
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.
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.
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:
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!
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!