Simple TDD in Erlang

Or how to use Pattern-Matching for Tests

Brujo Benavides
Erlang Battleground
5 min readMay 8, 2020

--

While acting as a mentor on the FutureLearn MOOC about Erlang I presented an idea that folks like Adolfo Neto loved (he even tweeted about it 🧡). It is, in fact, the way I introduce people to pattern-matching when I’m teaching them Erlang. It’s a way to write tests that let you naturally work your code out from them… Sounds familiar? Yes! It’s Test-Driven Development!

Airplane! (1980)

The Process

I learned TDD when I learned Smalltalk. It was such a life-changing lesson! And Smalltalk was the best environment to learn it since it’s built for it. In Smalltalk, I believe it’s actually harder to write code with a different process. If you didn’t try it already, you definitely should! Follow Hernan Wilkinson, his company (10pines) delivers Smalltalk courses where you can learn this from the experts, among several other great things.

Anyway, for the uninitiated, the general process for TDD is…

Original Here

What’s not described in the graph (particularly for compiled languages, like Erlang) is that after adding a test, in order to see that test fail, you first need the code to compile. And that, at first hand, may seem difficult. But it’s actually possible, using a nice hot-code-loading related feature of Erlang.

Let’s dive in with an example…

Do we have an extra day?

So, let’s say you have to write a module year with a single function: is_leap/1 . As you might guess, the idea is for that function to receive an integer and return a boolean telling you if it’s a leap year or not.

Add Tests

For the first step, we will already use the magic trick I mentioned above. Look at the module we’ll write…

-module year.
-export [test/0].
test() ->
false = year:is_leap(1),
ok.

Let’s dissect this thing…

The first two lines should be pretty obvious. We define the module and export test/0, which is the function we’ll use to run the tests.

Then we write the test/0 function. That function will just contain a series of assertions like most test functions do, specifying the expected results of our function is_leap/1.

The first assertion says that 1 is not a leap year. But pay attention to the fact that even when we’re going to eventually define is_leap/1 in this same module, we’re using the fully-qualified version of it (year:is_leap(…)) instead of just writing is_leap(1). Why? Hot-Code Reloading, of course!

Ron Damón as confused as you

The Erlang compiler will fail if it finds a call to an internal function that is not defined, it won’t compile your module and therefore it won’t run the test. Except… if you use a fully-qualified call. Because thanks to the logic behind hot code loading, you can totally define that function in a later version of the module.

That is super-useful for us since now we can…

See Tests Fail

1> c(year).
{ok, year}
2> year:test().
** exception error: undefined function year:is_leap/1
in function year:test/0 (year.erl, line 5)

Write Code

Great! Now our test tells us that we need to define is_leap/1 … So we do!

-module year.
-export [test/0].
-export [is_leap/1].
test() ->
false = year:is_leap(1),
ok.
is_leap(_) -> false.

Run Tests

We try compiling and running our tests in a more succinct way so we can use up arrow each time we go to the console… oh, lazy lazy developers!

3> c(year), year:test().
ok.

Rinse and Repeat…

Well… this is good! Now let’s add more assertions to our tests…

-module year.
-export [test/0].
-export [is_leap/1].
test() ->
% regular years are not leap
false = year:is_leap(1),
false = year:is_leap(1995),
false = year:is_leap(1997),
% multiples of 4 are leap…
true = year:is_leap(1996),
true = year:is_leap(2004),
true = year:is_leap(2008),
% …except if they're multiples of 100…
false = year:is_leap(1800),
false = year:is_leap(1900),
false = year:is_leap(2100),
% …except if they're also multiples of 400…
true = year:is_leap(1600),
true = year:is_leap(2000),
true = year:is_leap(2400),

ok.
is_leap(_) -> false.

…aaand…

5> c(year), year:test().
** exception error: no match of right hand side value false
in function year:test/0 (year.erl, line 11)

Line 11 is the one for 1996 (as expected)… We now fix our code for that case, run the tests again… Many iterations go by… etc…

I’ll jump to the final version for brevity…

-module year.
-export [test/0].
-export [is_leap/1].
test() ->
% regular years are not leap
false = year:is_leap(1),
false = year:is_leap(1995),
false = year:is_leap(1997),
% multiples of 4 are leap…
true = year:is_leap(1996),
true = year:is_leap(2004),
true = year:is_leap(2008),
% …except if they're multiples of 100…
false = year:is_leap(1800),
false = year:is_leap(1900),
false = year:is_leap(2100),
% …except if they're also multiples of 400…
true = year:is_leap(1600),
true = year:is_leap(2000),
true = year:is_leap(2400),
ok.
is_leap(Year) ->
is_leap(Year rem 4, Year rem 100, Year rem 400).
is_leap(_, _, 0) -> true;
is_leap(_, 0, _) -> false;
is_leap(0, _, _) -> true;
is_leap(_, _, _) -> false.

et voilà…

7> c(year), year:test().
ok

More Complex Stuff

Of course, this technique only works for very small pieces of code that should not reach production. You don’t want that test code messing up your releases!

But if you choose Common Test as your testing framework, a suite for this module will look eerily familiar…

-module year_SUITE.-export [all/0].
-export [is_leap/1].
all() -> [is_leap].is_leap(_) ->
% regular years are not leap
false = year:is_leap(1),
false = year:is_leap(1995),
false = year:is_leap(1997),
% multiples of 4 are leap…
true = year:is_leap(1996),
true = year:is_leap(2004),
true = year:is_leap(2008),
% …except if they're multiples of 100…
false = year:is_leap(1800),
false = year:is_leap(1900),
false = year:is_leap(2100),
% …except if they're also multiples of 400…
true = year:is_leap(1600),
true = year:is_leap(2000),
true = year:is_leap(2400),
{comment, ""}.

What do you think? Did I convince you to use Test-Driven Development for Erlang? 😉

--

--