Unit testing in Erlang with EUnit

To write unit tests in Erlang, you can use EUnit, xUnit’s erlang sibbling.

Writing unit tests in Erlang is really fun and you can do some pretty neat stuff. We will start at the very beginning but I will end up showing you how you can write a function so Erlang generates tests for you!

To start off, you can write your tests in the module you are testing or another separate module. Coming from C#, my first thoughts were that the tests should always be in a different file to separate the two concerns (testing and the actual code).

After playing a bit with EUnit, my opinion has shifted and I think both approaches have their merits and I will use the two in conjunction going forward.

For this blog I have created a simple module to test. These functions just double and triple an input parameter and my_reverse is a custom implementation of lists:reverse. I wrote simple functions as I did not want to focus on the module under test but rather on how to write the tests themselves.

Here is the module in question:

-module(sut).
-export([double/1, triple/1, my_reverse/1]).

double(X) -> X * 2.

triple(X) -> X * 3.

my_reverse(X) -> my_reverse(X, []).
my_reverse([], Acc) -> Acc; 
my_reverse([Head | Tail], Acc) -> my_reverse(Tail, [Head | Acc]).

For the test module, you should define a module with the same name as the one you are testing plus the _tests suffix. Following this convention will allow EUnit to find the tests of your module without referring to the test module (for example if you have tests embedded in you normal module as well as tests defined in an external module).

This module should contain an include of EUnit just after your -module declaration:

-include_lib("eunit/include/eunit.hrl").

Simple test functions

You can define simple test functions likewise:

% simple test functions
call_double_test() -> sut:double(2).
double_2_test() -> 4 = sut:double(2).
double_4_test() -> 8 = sut:double(4).
double_fail_test() -> 7 = sut:double(3).

The functions need to have the _test suffix which will allow EUnit to find and run them. While you could do similar tests functions without EUnit, EUnit affords you the convenience of easily running and getting feedback on your tests. Also as I will cover afterwards, EUnit allows for better ways to write test functions.

Here the call_double_test will only check that the function doesn’t crash while the others will use patter matching to verify the results.

To run your tests, just compile your test module and then call the test() function. This function is made available when you import EUnit.

Here is a complete example if you want to follow along:

-module(sut_tests).
-include_lib("eunit/include/eunit.hrl").

% simple test functions
call_double_test() -> sut:double(2).
double_2_test() -> 4 = sut:double(2).
double_4_test() -> 8 = sut:double(4).
double_fail_test() -> 7 = sut:double(3).

Calling the tests:

15> sut_test:test().

And here is the output:

sut_test: double_fail_test...*failed*
::error:{badmatch,6}
  in function sut_test:double_fail_test/0


=======================================================
  Failed: 1.  Skipped: 0.  Passed: 3.
error

Assert macros

The next step is to use assert macros. These are a step above the test functions and make the assertions more readable and more xUnit like.

% assert macros
triple_3_test() -> ?assert(sut:triple(3) =:= 9).
triple_fail_test() -> ?assert(sut:triple(3) =:= 10).

Note that there is a plethora of other macros including assertNot and assertMatch. You can consult the EUnit documentation for a full list.

Test generators

This is where the fun begins. Before, we specified functions (or macros) that tested the module. With test generators we can specify a list of tests and EUnit will run all of these.

Test generators use the _test_ suffix rather than _test. The test macros themselves also have a leading underscore, ie: ?_assert, rather than ?assert.

We could have a generator that generates a single test:

double_gen_test_() -> ?_assert(sut:double(3) =:= 6).

Or to minimize typing we could group all related tests in a single list likewise:

double_gens_test_() -> [?_assert(sut:double(2) =:= 4),
						?_assert(sut:double(3) =:= 6),
						?_assert(sut:double(4) =:= 8),
						?_assert(sut:double(5) =:= 10)].

In this previous example we have grouped four test functions in a single list.

This is ok, I guess, but it also opens up the possibility for something else…

Programmatically generating your tests

Since test generators operate on lists of tests, we can use regular list comprehension to programmatically create our list.

Here is a first example:

double_gen_test_() -> [?_assert(sut:double(X) =:= X * 2) || X <- lists:seq(1, 10)].

As the output shows:

62> sut_tests:test().
  All 10 tests passed.
ok

This single line of code generated ten tests. Call double with values 1 through 10 and check the output.

Next consider a list comprehension that creates a list of lists to test our reverse function:

reverse_gen2_test_() -> [?_assert(sut:my_reverse(List) =:= lists:reverse(List)) || List <- [lists:seq(1, Max) || Max <- lists:seq(1, 10)]].

If we break this one apart,

[lists:seq(1, Max) || Max <- lists:seq(1, 10)].

Will generate the following output:

[[1],
 [1,2],
 [1,2,3],
 [1,2,3,4],
 [1,2,3,4,5],
 [1,2,3,4,5,6],
 [1,2,3,4,5,6,7],
 [1,2,3,4,5,6,7,8],
 [1,2,3,4,5,6,7,8,9],
 [1,2,3,4,5,6,7,8,9,10]]

Then it will compare our implementation of my_reverse with Erlang’s lists:reverse for all ten lists.

Even tough this is really cool, be careful not to abuse this, as most of time simple tests will be much clearer.

But in some situations where you can define a function to generate loads of data, using list comprehension can be a time-saving solution.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s