Elixir's built-in documentation and testing Tools

car in a wind tunnel testing
image source

TL;DR.

  • Elixir has an amazing built-in feature that let’s you test code inside the documentation.
  • You don’t need to install any tool to write your tests, it’s built-in in the language!
  • Elixir has an official dependency that generates a beautiful HTML page for your docs.

One of the first and most impressive characteristics I saw when I started to study Elixir was the powerful yet simple built-in tools to help developers write documentation and tests.

A very special feature is the called doctest, which is a test that lives inside your documentation. With such tests, you can assure your docs are up-to-date, because whenever you run your tests, the tests inside your documentation will also be verified. Really awesome!

In this article I’ll show you how to write a simple program with unit tests and how to document your code and generate a beautiful HTML documentation page.

Let’s start!

You can see the source code here.

Specification

Our program should implement the solution for the staircase problem.

The program will receive a positive number n and should print a staircase right-aligned composed of # symbols and spaces, with a height and width equals to n.

Example:

1
2
3
4
5
6
7
8
input > 6
output >
#
##
###
####
#####
######

Creating the project

To create the project, type the following command in your terminal:

1
mix new staircase

With the structure generated by mix we already can write our program and our test files. Let’s now install the ExDoc library that will generate the HTML documentation page for us.

Installing ExDoc

Go to mix.exs file and add the following line inside the deps function:

mix.exs
1
2
3
4
5
defp deps do
[
{:ex_doc, "~> 0.16"}
]
end

Now all you need to do is run the command mix deps.get to install the dependency.

Writing the tests

First we’ll write some tests so we’ll be able to easily verify if our solution is actually satisfying our specs.

To do this, go to the already created file inside the test folder staircase_test.exs.

All test files end up with the .exs extension. The difference between .ex and .exs is that files with the .exs won’t be compiled, they’re Elixir scripts.

Let’s remove the original test case and add our assertions there:

test/staircase_test.exs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
defmodule StaircaseTest do
use ExUnit.Case
import ExUnit.CaptureIO
doctest Staircase
test "if staircases have the right size" do
assert capture_io(fn -> Staircase.main(1) end) == "#\n"
assert capture_io(fn -> Staircase.main(2) end) ==
" #
##\n"
assert capture_io(fn -> Staircase.main(3) end) ==
" #
##
###\n"
assert capture_io(fn -> Staircase.main(7) end) ==
" #
##
###
####
#####
######
#######\n"
end
end

Don’t panic! Those tests have some interesting peculiarities and it’ll be a good opportunity to learn them. I know it’s looking a little ugly but I hope after the explanation you’ll be more comfortable with such assertions.

The first piece to note is the import ExUnit.CaptureIO. This module gives to us the ability to capture the output when a function prints something so we can make an assertion to that output. You can learn more about such module here.

Another interesting part is the doctest Staircase. This tells that we will run the tests inside the documentation of the module Staircase together with this suit of tests.

Inside our test case, we can see that the capture_io function, from the ExUnit.CaptureIO module, receives an anonymous function (fn -> end). This is how that function works: you should pass an anonymous functions to it so it’ll be called later. Check the documentation linked before to see more ways to use this function.

To finish the explanation of our tests, the string representing the output was written in the line below of our assertion because in that way the spaces before each line wouldn’t mess/broke our assertion and it’s more intuitive to see the final result.

You can see though that Elixir allows us to write strings in multiple lines.

Writing the module

Breaking the specification more granularly, we have to:

  1. Receive an integer corresponding to the size n of the staircase
  2. Generate a string with an arbitrary size containing spaces
  3. Generate a string with an arbitrary size containing # characters
  4. Generate a row appending the space string and the char string
  5. A row should be made of n characters, counting spaces and #
  6. Print such row

A possible solution to accomplish such task could be:

  1. Create a module Staircase
  2. Create a main function to receive the input and print each row to the output
  3. Create a function to generate a string of an arbitrary size composed with an arbitrary char
  4. Append the string made with spaces and the string made with #
  5. Print the final string

Here is my solution for such task. Such code should be copied to the staircase.ex file, inside the lib folder.

lib/staircase.ex
1
2
3
4
5
6
7
8
9
10
11
12
defmodule Staircase do
def main(size) do
Enum.each(1..size, fn i ->
IO.puts(string_gen(size - i, " ") <> string_gen(i, "#"))
end)
end
def string_gen(0, _), do: ""
def string_gen(size, char) when size > 0 do
Enum.reduce(1..size, "", fn(_i, acc) -> acc <> char end)
end
end

Now that we have our final solution, let’s run our tests to see if it’s working fine.

Typing mix test in your terminal you should see the following message:

1
2
3
4
5
6
7
> mix test
.
Finished in 0.03 seconds
1 test, 0 failures
Randomized with seed 477889

Great! Now we are almost done. Let’s finish writing some documentation and the most exciting part, the doctests :)

Documentation and doctests

From the Elixir docs:

Elixir treats documentation as a first-class citizen. This means documentation should be easy to write and easy to read.

We can easily confirm this statement. Elixir gives to us the module attributes @moduledoc and @doc so we can write documentation for our modules and functions. You can write in markdown inside your documentation as well.

Let’s see it in practice.

In the staircase.ex file, below your module definition, let’s add a @moduledoc.

lib/staircase.ex
1
2
3
4
5
6
7
defmodule Staircase do
@moduledoc """
Generate staircases based on the input value
"""
# ...
end

Now let’s describe what our main function does:

lib/staircase.ex
1
2
3
4
5
6
7
8
@doc """
Receives the input and print the staircase to the console
"""
def main(size) do
Enum.each(1..size, fn i ->
IO.puts(string_gen(size - i, " ") <> string_gen(i, "#"))
end)
end

And finally, let’s describe what our string_gen function does and write some tests inside our documentation!

lib/staircase.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@doc """
Generate a string of size `size` made with characters `char`
## Example
iex> Staircase.string_gen(0, "#")
""
iex> Staircase.string_gen(1, "#")
"#"
iex> Staircase.string_gen(7, "#")
"#######"
iex> Staircase.string_gen(0, " ")
""
iex> Staircase.string_gen(1, " ")
" "
iex> Staircase.string_gen(7, " ")
" "
"""
def string_gen(0, _), do: ""
def string_gen(size, char) when size > 0 do
Enum.reduce(1..size, "", fn(_i, acc) -> acc <> char end)
end

This last part receives our attention. In order to write code that will be evaluated when you run your tests, you have to put inside your @doc the ## Example part exactly in the way it was done here:

  1. Add ## Example
  2. Skip a line
  3. Add 4 spaces considering the column where ## Example started
  4. Add iex> and the function/code that will be tested
  5. Write what is the expected return of such function in the line below

Let’s run mix test so you can see we have now 7 tests. Pretty cool!

1
2
3
4
5
6
7
8
> mix test
Compiling 1 file (.ex)
.......
Finished in 0.07 seconds
7 tests, 0 failures
Randomized with seed 70528

Changing the second assertion to intentionally break the test we can see how helpful and smart ExUnit is.

1
2
3
4
5
6
7
8
9
10
11
12
1) test doc at Staircase.string_gen/2 (2) (StaircaseTest)
test/staircase_test.exs:4
Doctest failed
code: Staircase.string_gen(1, "#") === "&"
left: "#"
stacktrace:
lib/staircase.ex:23: Staircase (module)
Finished in 0.07 seconds
7 tests, 1 failure

It tells exactly what and where the problem is. Amazing stuff, serious.

Generating the documentation page

Our final step is to generate our beautiful documentation page using the ExDoc tool we installed in the beginning of the article.

It’s extremely easy to do so, just run mix docs and you’re done!

1
2
3
4
> mix docs
Compiling 1 file (.ex)
Docs successfully generated.
View them at "doc/index.html".

Go to doc/index.html and open the file in your favorite browser to navigate in your docs!

OBS: you can activate the night mode in the end of the page clicking in “Switch to night mode”.

If you want some tips on how to learn Elixir or any other programming language, this article can be helpful.

That’s it! I hope you enjoyed that simple demonstration of the helpful features Elixir brings to us for free.

Until next ;D