Test-First Word Wrap in Erlang
I'm continuing to play with Erlang. This week, I needed to write some code
that extracts information from a bunch of source files. Part of the output was
supposed to be a list of strings, nicely word wrapped to fit on a terminal screen.
I decided to experiment with using a test-first approach to developing the word wrap function. I wrote unit tests that describe the functionality I want, and then wrote some code to make the tests run successfully.
To do that, I used the new EUnit-2.0 unit testing framework. It's
still under development, but it seems to work nicely (although I'd love for it to
have an assert_equal method).
Installing EUnit
First, we'll install EUnit from their Subversion repository:
[dave/Erlang/common] svn co http://svn.process-one.net/contribs/trunk/eunit A eunit/Emakefile A eunit/sys.config A eunit/include : : : [dave/Erlang/common] cd eunit [Erlang/common/eunit] make : : :
Now we need to add the EUnit library into your default Erlang path. Edit the file
.erlang in your home directory (create it if it's not there already)
and add the line
code:add_pathz("/Users/dave/Erlang/common/eunit/ebin").
(You'll need to change the path to reflect the location where you downloaded EUnit.
Remember to add the /ebin part at the end. And, yes, the “z” is really
path of that line.)
EUnit In 60 Seconds
EUnit basically lets you run assertions against Erlang code. You can use it in a number of different modes. In our case we'll include the tests in the same file that contains the code we're testing. This is very convenient, but longer term it has a downside—anyone using your code will also need to have EUnit installed, even if they don't want to run the tests. So, later we can split the tests into their own file.
The key to using EUnit is including the library in the source file containing the tests. That's as simple as adding the line:
-include_lib("eunit/include/eunit.hrl").
Any module that includes this line will automatically have a new function called
test/0 defined. This test function will automatically run
all the tests in the module.
So, how do you define tests? As with any xUnit framework, you can write tests by
defining regular methods that follow a naming convention. With EUnit, the convention
is that the function name should end _test. The EUnit convention is
that any test function that returns is considered to have completed successfully;
any function that throws an exception is a failure. It's common to use pattern
matching as a kind of assertion:
length_test() -> 3 = length("cat").
reverse_test() -> [3, 2, 1] = lists:reverse([1,2,3]).
However, for simple tests, it's easier to write a test generator. This is a function
whose name ends _test_ (with a final underscore). Test generators
return a representation of the tests to be run. In our case, we'll return a list of
tests created using the EUnit _assert macro. We can rewrite our previous two
test functions using a test generator called demo_test_:
demo_test_() -> [
?_assert(3 == length("cat")),
?_assert([3, 2, 1] == lists:reverse([1,2,3]))
].
Here the generator returns an array containing two tests. The tests are written
using the _assert macro. This takes a boolean expression (rather than
a pattern) which is why we're now using two equals signs.
Word Wrap
My application needs a library function which takes an array of strings and joins
them together to form an array of lines where no line is longer than a given
length. We'll write a function wrap/1 in a module called
text. Let's start with a basic test. If you wrap no words, you get a
single empty line.
-module(text).
-export([wrap/1]).
-include_lib("eunit/include/eunit.hrl").
wrap_test_() -> [
?_assert(wrap([]) == [""])
].
This won't compile: the wrap/1 function hasn't been defined. Getting it
to pass this single test isn't hard:
wrap([]) ->
[ "" ].
We can run this in the Erlang shell:
1> c(text).
{ok,text}
2> text:test().
Test successful.
ok
Let's add the next test. Wrapping a single short word should result in a single line containing that word.
wrap_test_() -> [
?_assert(wrap([]) == [""]),
?_assert(wrap(["cat"]) == ["cat"])
].
The tests now fail when we run them: our wrap function doesn't yet know
how to wrap strings:
1> c(text).
{ok,text}
2> text:test().
text:9:wrap_test_...*failed*
::{error,function_clause,
[{text,wrap,[["cat"]]},{text,'-wrap_test_/0-fun-2-',0}]}
=======================================================
Failed: 1. Aborted: 0. Skipped: 0. Succeeded: 1. error
Let's see if we can fix this. We'll add a second version of the wrap/1
function that knows how to wrap a single string. Do do this, we'll use a key feature
of Erlang.
Pattern Matching and Function Definitions
In the same way that Erlang uses pattern matching when evaluating the =
operator, it also uses pattern matching when invoking functions. This is often
illustrated with an inefficient implementation of a function to calculate the nth
term in the Fibonacci sequence (the sequence that goes 0, 1, 1, 2, 3, 5, ..., where
each term is the sum of the preceding two terms).
-module(fib). -export([fib/1]). fib(0) -> 1; fib(1) -> 1; fib(N) -> fib(N-1) + fib(N-2).
Here we have three definitions of the same function (note how they're separated by semicolons, and terminated with a period). The first two match when the function is called with a parameter or zero or one. The third is called with any other parameter.
We can use this to define two versions of our wrap/1 function, one for
the case where it is called with no words in the list, the other when we have a
single string.
wrap([]) ->
[ "" ];
wrap([String]) ->
[ String ].
The tests now pass.
9> text:test().
All 2 tests successful.
Add a test which wraps two strings, though, and our tests again fail:
wrap_test_() -> [
?_assert(wrap([]) == [""]),
?_assert(wrap(["cat"]) == ["cat"]),
?_assert(wrap(["cat", "dog"]) == ["cat dog"])
].
text:10:wrap_test_...*failed*
::{error,function_clause,
[{text,wrap,[["cat","dog"]]},{text,'-wrap_test_/0-fun-4-',0}]}
We're going to have to do some work. For a start, we're going to have to rewrite our wrap methods to give them somewhere to store the result. Then they're going to have to recurse over the input parameter list, adding each word to the output until the input is exhausted.
Building an Output List
We're going to do two things here. The externally visible function,
wrap/1, is simply going to invoke an internal function,
wrap/2, passing it the word list and an addition parameter. This second
parameter is the list that holds the results. Remember that in Erlang the number of
arguments is part of a function's signature, so wrap/1 and
wrap/2 are separate functions.
Then we're going to use our parameter matching trick to define two variants of
wrap/2:
% This is the exported function: it passes the initial
% result set to the internal versions
wrap(Words) ->
wrap(Words, [""]).
% When we run out of words, we're done
wrap([], Result) ->
Result;
% Otherwise append the next word to the result
wrap([NextWord|Rest], Result) ->
wrap(Rest, [ Result ++ NextWord]).
The second version of wrap/2 uses a neat feature of Erlang list
notation. The pattern [NextWord|Rest] matches a list whose head is
matched with NextWord and whose tail is matched with Rest.
If we invoked this function with:
wrap([ "cat", "dog", "elk" ], Result).
then NextWord would be set to "cat" and Rest
would be set to ["dog", "elk"].
Unfortunately, this fails our test:
1> c(text).
{ok,text}
2> text:test().
text:10:wrap_test_...*failed*
::{error,{assertion_failed,[{module,text},
{line,10},
{expression,
"wrap ( [ \"cat\" , \"dog\" ] ) == [ \"cat dog\" ]"},
{expected,true},
{value,false}]},
[{text,'-wrap_test_/0-fun-4-',0}]}
=======================================================
Failed: 1. Aborted: 0. Skipped: 0. Succeeded: 2.
This is where I wish EUnit had an assert_equals function that could
report the actual and expected values. However, I can still invoke my wrap method
from the shell to see what it's returning. (Stop press: EUnit does have the equivalent, and I didn't notice it. It's called assertMatch(Expected, Actual). Sorry.)
3> text:wrap(["cat", "dog"]). ["catdog"]
Oops. We forgot the space between words. We need to add this when we add a word to an existing line, but only if that line is not empty. See how we use pattern matching to distinguish a list containing an empty line from one containing a non-empty one.
% This is the exported function: it passes the initial
% result set to the internal versions
wrap(Words) ->
wrap(Words, [""]).
% When we run out of words, we're done
wrap([], Result) ->
Result;
% Adding a word to an empty line
wrap([NextWord|Rest], [ "" ]) ->
wrap(Rest, [ NextWord]);
% or to a line that's already partially full
wrap([NextWord|Rest], [ CurrentLine ]) ->
wrap(Rest, [ CurrentLine ++ " " ++ NextWord]).
Now our tests pass again:
1> c(text).
{ok,text}
2> text:test().
All 3 tests successful.
ok
When Clauses
For testing purposes, let's assume that we wrap lines longer than 10 characters. That means that if we give our method the strings "cat", "dog", and "elk", we'll expect to see two lines in the output (because "cat<space>dog<space>elk" is 11 characters long). Time for another test.
wrap_test_() -> [
?_assert(wrap([]) == [""]),
?_assert(wrap(["cat"]) == ["cat"]),
?_assert(wrap(["cat", "dog"]) == ["cat dog"]),
?_assert(wrap(["cat", "dog", "elk"]) == ["cat dog", "elk"])
].
We don't even have to run this to know it will fail: our wrap function
knows nothing about line lengths. Time for some code. We're now going to have to
treat out result as a list or strings, rather than a single string. We're also going
to have to deal with the case where there's not enough room in the current output
line for the next word. In that case we have to add a new empty line to the
function's result and put the word into it.
wrap(Words) ->
wrap(Words, [""]).
% When we run out of words, we're done
wrap([], Result) ->
Result;
% Adding a word to an empty line
wrap([NextWord|Rest], [ "" | PreviousLines ]) ->
wrap(Rest, [ NextWord | PreviousLines ]);
% or to a line that's already partially full
% there are two cases:
% 1. The word fits
wrap([NextWord|Rest], [ CurrentLine | PreviousLines ])
when length(NextWord) + length(CurrentLine) < 10 ->
wrap(Rest, [ CurrentLine ++ " " ++ NextWord | PreviousLines ]);
% 2. The word doesn't fit, so we create a new line
wrap([NextWord|Rest], [ CurrentLine | PreviousLines ]) ->
wrap(Rest, [ NextWord, CurrentLine | PreviousLines ]).
10>
This introduces a new Erlang feature—you can further qualify the pattern
matching used to determine which function is selected using a when
clause. In our case, the parameter patterns for the last two definitions of the
wrap/2 method are the same. However, the first of them has a
whenclause
. This means that this function will only be selected if the length of the current output line plus the length of the next word is less that our maximum line length (hard coded to 10 in this case).
Unfortunately, our tests fail:
1> text:test().
text:11:wrap_test_...*failed*
::{error,{assertion_failed,[{module,text},
{line,11},
{expression,
"wrap ( [ \"cat\" , \"dog\" , \"elk\" ] ) == [ \"cat dog\" , \"elk\" ]"},
{expected,true},
{value,false}]},
[{text,'-wrap_test_/0-fun-6-',0}]}
=======================================================
Failed: 1. Aborted: 0. Skipped: 0. Succeeded: 3.
error
Running text:wrap manually shows why:
2> text:wrap(["cat", "dog", "elk"]). ["elk","cat dog"]
We're building the result list backwards, adding each successive line to the front
of it, rather than the back. We could fix that by changing the way we add lines to
the list, but, like Lisp, Erlang favors list manipulations that work on the head of
the list, not the last element. It turns out to be both idiomatic and more efficient
to build the result the way we're doing it, and then to result the resulting list
before returning it to our caller. That's a simple change to our exported
wrap/1 function.
wrap(Words) ->
lists:reverse(wrap(Words, [""])).
Now all out tests pass.
So, let's review our tests. We have covered an empty input set, a set that fits on one line, and a set that (just) forces the result to take more than one line. Are there any other cases to consider? It turns out that there's (at least) one. What happens if a word is too long to fit on a line on its own? Let's see:
wrap_test_() -> [
?_assert(wrap([]) == [""]),
?_assert(wrap(["cat"]) == ["cat"]),
?_assert(wrap(["cat", "dog"]) == ["cat dog"]),
?_assert(wrap(["cat", "dog", "elk"]) == ["cat dog", "elk"]),
?_assert(wrap(["cat", "dog", "hummingbird", "ibix"]) == ["cat dog", "hummingbird", "ibix"])
].
Run the tests:
1> c(text).
{ok,text}
2> text:test().
All 5 tests successful.
ok
Wrapping Up
Using simple tests like this are a great way of designing some code. They're also a goo way to teach yourself the language. As I was writing this code, I used the tests to test my understanding of Erlang semantics.
The Final Program
-module(text).
-export([wrap/1]).
-include_lib("eunit/include/eunit.hrl").
wrap_test_() -> [
?_assert(wrap([]) == [""]),
?_assert(wrap(["cat"]) == ["cat"]),
?_assert(wrap(["cat", "dog"]) == ["cat dog"]),
?_assert(wrap(["cat", "dog", "elk"]) == ["cat dog", "elk"]),
?_assert(wrap(["cat", "dog", "hummingbird", "ibix"]) == ["cat dog", "hummingbird", "ibix"])
].
% This is the exported function: it passes the initial
% result set to the internal versions
wrap(Words) ->
lists:reverse(wrap(Words, [""])).
% When we run out of words, we're done
wrap([], Result) ->
Result;
% Adding a word to an empty line
wrap([ NextWord | Rest ], [ "" | PreviousLines ]) ->
wrap(Rest, [ NextWord | PreviousLines ]);
% Or to a line that's already partially full. There are two cases:
% 1. The word fits
wrap([ NextWord | Rest ], [ CurrentLine | PreviousLines ])
when length(NextWord) + length(CurrentLine) < 10 ->
wrap(Rest, [ CurrentLine ++ " " ++ NextWord | PreviousLines ]);
% 2. The word doesn't fit, so we create a new line
wrap([ NextWord | Rest], [ CurrentLine | PreviousLines ]) ->
wrap(Rest, [ NextWord, CurrentLine | PreviousLines ]).





There are some typos in The Final Program: all ?assert should've been instead ?_assert.
You may use assertMatch(expected, actual) macro for the purpose of assertEqual in xUnit frameworks.
Posted by: June Kim | April 20, 2007 at 11:23 AM
June Jim: Sorry--bug in my markup. Should be OK now.
I didn't spot assertMatch: I'll investigate and maybe reissue the article.
Posted by: Dave Thomas | April 20, 2007 at 11:33 AM
You are using the generator syntax here. For most simple tests its better to just use the more strait forward non-generator syntax. If you put each of your tests in their own function, making sure that the name ends in _test, eunit will wrap them up and do all the set up for you. It also lets eunit give a bit better error messages (including the test function name, a pretty important thing) and lets you run the tests individually.
So you could do
wrap_empty_test() ->
?assertMatch([""], wrap([])).
wrap_one_test() ->
?assertMatch(["cat"], wrap(["cat"])).
wrap_two_test() ->
?assertMatch(["cat dog"], wrap(["cat", "dog"])).
wrap_three_test() ->
?assertMatch(["cat dog", "elk"], wrap(["cat", "dog", "elk"])).
wrap_four_test() ->
?assertMatch(["cat dog", "hummingbird", "ibix"], wrap(["cat", "dog", "hummingbird", "ibix"])).
Eunit will then handle wrapping them up in a function called 'test' and exporting them. These examples are a bit simple but for any tests of even moderate complexity this approach works well.
Posted by: Eric Merritt | April 20, 2007 at 12:20 PM
Eric:
Thanks for the comment. I certainly would have used assertMatch had realized I could have. But I'm interested: why are the individual methods better than the more concise generator form? I kinda like the fact that it is lighter weight than the method-based approach.
Dave
Posted by: Dave Thomas | April 20, 2007 at 01:18 PM
Two reasons really, none of which is a show stopper. In the error output for the generator form only the module name and line are given to reference the error by. In the non-generator form the actual name of the function is given. This gives you an immediate insight onto what failed without having to do any further digging. For example, its the difference between a first line of
text:11:wrap_test_...*failed*
and
text:wrap_empty_test...*failed*
The first one tells you that something in your generator failed but not what. The second gives you the specific test function that failed.
The other issue is with debugging. The generator version has a default timeout set, so the test will timeout before you ever get the debugger brought up. Where as if you just run the test function directly no timeout is involved. You can change this timeout but its not very strait forward and I always forget to do it until after I run into the problem.
Also when debugging its very nice to be able to run just the test that is failing, which is harder to do in the generator model.
You don't have to put a single assertMatch under a function. This could just as easily have been set up as
wrap_empty_test() ->
?assertMatch([""], wrap([])).
wrap_nonempty_test() ->
?assertMatch(["cat"], wrap(["cat"])),
?assertMatch(["cat dog"], wrap(["cat", "dog"])),
?assertMatch(["cat dog", "elk"], wrap(["cat", "dog", "elk"])),
?assertMatch(["cat dog", "hummingbird", "ibix"], wrap(["cat", "dog", "hummingbird", "ibix"])).
The important thing from a testing standpoint is knowing the locality of the error *from the command line* and being able to debug it.
Posted by: Eric Merritt | April 20, 2007 at 02:18 PM
You can as well give descriptive names when using generators.
wrap_test_()-> [
{"empty" ,?_assertMatch([""] , wrap([]))},
{"single",?_assertMatch(["cat"], wrap(["cat"]))}
].
Each assert will run independently, as if there were in separate functions.
Posted by: June Kim | April 20, 2007 at 02:51 PM
Hi! Nice article!
I just want to clarify that there is _no_ difference in timeout handling between tests produced from generators and tests that are written as individual functions. (Well, it shouldn't be. If there is any detectable difference, it's a bug.)
Also, the dependency-on-EUnit-when-compiling problem is typically fixed by enclosing the test code sections in -ifdef(TEST). ... -endif. sections (or equivalently, -ifdef(EUNIT).)
Happy hacking!
Posted by: Richard Carlsson | April 20, 2007 at 03:51 PM
Richard,
You are absolutely right. When calling the 'test' module generated by eunit there is no difference at all between those using generators and those not using generators. However, not using generators allows you to call the *_test function directly. In this case there is no timeout involved. I guess I wasn't as clear as I should have been on that one.
June,
I didn't realize that. Thanks for the heads up.
Posted by: Eric Merritt | April 20, 2007 at 05:38 PM
This post contains a character that makes your feed malformed:
http://feedvalidator.org/check.cgi?url=http%3A%2F%2Fpragdave.pragprog.com%2Fpragdave%2Frss.xml
Please consider removing the character. Thanks! :)
Posted by: Christoffer Sawicki | April 21, 2007 at 04:54 PM
Since I am on Windows, I got stuck where you say to run 'make'.
I could not find a working make clone for Windows, so I worked around it as follows ...
I have svn for Windows so did the following with no trouble:
svn co http://svn.process-one.net/contribs/trunk/eunit
Then changed the directory to the resulting eunit dir and typed (all on one line):
for %f in (src/*.erl) do erlc -pa ebin -Iinclude +warn_unused_vars +nowarn_shadow_vars +warn_unused_import -oebin src/%f
That compiles them all and directs the results to
the ebin directory. Then I copied the whole eunit
subtree over into
C:\Program Files\erl5.5.4\lib
(where I have erlang installed).
Having done that, it all works perfectly with the code you have.
( Since I have no home directory with an .erlang file,
I had nowhere to put such advice as
code:add_pathz("/Users/dave/Erlang/common/eunit/ebin"). )
-- Mike Berrow
Posted by: Mike Berrow | May 06, 2007 at 08:54 PM
Mike,
this _almost_ seems to work, except the compile for eunit_tests.erl failing at eunit_autoexport -- and as a result it seems that EUnit doesn't seem to know about exporting a test() function. Did anyone stumble into a similar problem on a WinXP-based machine and has found a work-around?
Posted by: Oliver Hofmann | May 20, 2007 at 02:25 PM
I got tripped up by the following code, taken from the article.
% This is the exported function: it passes the initial
% result set to the internal versions
wrap(Words) ->
wrap(Words, [""]).
% When we run out of words, we're done
wrap([], Result) ->
Result;
% Otherwise append the next word to the result
wrap([NextWord|Rest], Result) ->
wrap(Rest, [ Result ++ NextWord]).
Look at that last line. Result is a list, Nextword is a string. Concatenating them does not do the right thing. The last clause should read
wrap([NextWord|Rest], [Result]) ->
wrap(Rest, [ Result ++ NextWord]).
Typo notwithstanding, great article. As a newcomer to Erlang, I found it very instructive.
Posted by: David Cabana | August 06, 2007 at 09:18 PM