« Adding Concurrency to Our Erlang Program | Main | Drawn to the Mike »

April 20, 2007

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83451c41c69e200d83453890469e2

Listed below are links to weblogs that reference Test-First Word Wrap in Erlang:

» Test-First Development in Erlang from devmates.com
Dave Thomas wrote a nice article about Test-First Development using Erlang and EUnit. In the article, he describes the whole development process beginning from the installation of EUnit to the final program. ... [Read More]

Comments

June Kim

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.

Dave Thomas

June Jim: Sorry--bug in my markup. Should be OK now.

I didn't spot assertMatch: I'll investigate and maybe reissue the article.

Eric Merritt

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.

Dave Thomas

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

Eric Merritt

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.

June Kim

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.

Richard Carlsson

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!

Eric Merritt

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.

Christoffer Sawicki

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! :)

Mike Berrow

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

Oliver Hofmann

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?

David Cabana

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.

Scott

I am trying to install on Ubuntu 8.04 and am getting the following error:

./eunit_data.erl:29: can't find include lib "kernel/include/file.hrl"
./eunit_data.erl:346: record file_info undefined
./eunit_data.erl:606: record file_info undefined
make[1]: *** [../ebin/eunit_data.beam] Error 1
make[1]: Leaving directory `/home/sspeidel/Software/eunit/src'
make: *** [subdirs] Error 2

Any Suggestions?

ofer

@scott
did you install erlang?

if not, run:

sudo apt-get install erlang

The comments to this entry are closed.

Now in Beta

  • Programming Ruby, 3rd Edition
    Third Edition, Covering Ruby 1.9, now available
My Photo

Pragmatic Stuff

Photos

  • www.flickr.com
    This is a Flickr badge showing public photos from pragdave tagged with pragdave_badge. Make your own badge here.