05.11.07

EUnit vs ErUnit

Posted in ErUnit at 8:15 am by BestFriendChris

I recently was pointed to this article by pragdave about test-first development in Erlang with EUnit. I’m going to use his examples to show you how it would be done in ErUnit instead. First, some history…

The reason I wrote ErUnit instead of just using EUnit is for a couple of reasons

  1. The best way to learn a language is to reimplement xUnit. There’s so much meta stuff going on that it really requires you to dig in the language. And when I say reimplement, I don’t mean, “Follow the xUnit spec exactly.” What I mean is leverage the things that make the language good to create a good fit.
  2. I didn’t see it on the web. Mostly because I assumed EUnit was for the E programming language. So, I searched for ErUnit and found nothing…
  3. Even after I found out about EUnit, it didn’t gel with me. Now, that might be due to there not being any good tutorials that I could find, but I just wasn’t seeing it. I don’t like using macros specifically for assertions as it makes the failures hard to debug.
  4. I really don’t like having your tests live in the same file as your production code… It makes your code much harder to understand if you have to double the size of the file just to test it…
  5. I’m not a big fan of the standard that I saw of using pattern matching for the tests. I personally think there should be a difference between a failed assertion and a failed pattern match within your code, and I don’t know how to make that distinction.

A huge benefit of going with EUnit is that it is MUCH more mature than ErUnit. I’m still tweaking it as need arises. One thing that I’m not sure if it really matters yet is all tests are run in their own thread, concurrently. I did that because it seemed super easy and fun to do, but I don’t know if you really get any benefit with that yet…

Well, on to creating a word wrap utility, test first, with ErUnit.

ErUnit in >60 Seconds

When creating a ErUnit test, you first name the file starting with “test_”. In our example below, when testing “text.erl”, we create the test file “test_text.erl”. ErUnit, with erunit_suite, has the convention of either:

  • If your code and tests are in the same folder, add that folder to the path
  • If your code is in the “ebin” folder and the tests are in the “ebin_tests” folder, adding both folders to the path

This allows you to break up the tests from the code it’s testing pretty easily. Within the test class, you have to add the erunit_test behaviour. Behaviours in Erlang (well, technically the OTP library in Erlang) are used to define what callback functions are required. In this case, the erunit_test callback requires a function call “tests” with no arguments to be exported.

I personally also add a no argument “run” function that just delegates to erunit:run() with the tests passed in. I also like to import all of the erunit functions I’m going to be using (test/2, assertEquals/2, run/1) and the functions I’m going to be testing. This makes the code later on easier to read.

The file so far is:

-module(test_text).

-behaviour(erunit_test).
-export([tests/0, run/0]).

-import(erunit, [test/2, assertEquals/2, run/1]).
-import(text, [wrap/1]).

run() ->
	run(tests()).

As mentioned earlier, you must declare a no argument function called “tests”. All this returns is an array of tests. So, for the first example we want to test that the “wrap” function works with no words.

tests() ->
		[
			test("Should wrap no words", fun() ->
				assertEquals([""], wrap([]))
			end)
		].

When you use the erunit:test/2 function, you pass in the description of what you’re testing and a fun/0 of the actual test. assert(Description, true-false-test), assertEquals(First, Second), assertEquals(Description, First, Second), and fail(Description) are all currently implemented. Behind the scenes, the “test” function spawns a new thread that will run the fun/0 and send the answer back to the current test runner thread.

The code for the “text” module is the same as in pragdave’s example, so I won’t repeat it here. In fact, I’m just going to show you the entire test file:

-module(test_text).

-behaviour(erunit_test).
-export([tests/0, run/0]).

-import(erunit, [test/2, assertEquals/2, run/1]).
-import(text, [wrap/1]).

run() ->
	run(tests()).

tests() ->
		[
			test("Should wrap no words", fun() ->
				assertEquals([""], wrap([]))
			end),

			test("Should wrap one word", fun() ->
				assertEquals(["cat"], wrap(["cat"]))
			end),

			test("Should wrap two words", fun() ->
				assertEquals(["cat dog"], wrap(["cat", "dog"]))
			end),

			test("Should wrap when more than 10 chars long", fun() ->
				Expected = ["cat dog", "elk"],
				assertEquals(Expected, wrap(["cat", "dog", "elk"]))
			end),

			test("Should allow word larger than 10 on its own line", fun() ->
				Expected = ["cat dog", "hummingbird", "ibix"],
				Actual = wrap(["cat", "dog", "hummingbird", "ibix"]),
				assertEquals(Expected, Actual)
			end)
		].

The output when running from erl is:

1> test_text:run().
.....
ok
2>

When a test fails with this extra test added:

			test("This will fail", fun() ->
				erunit:fail("Called erunit:fail/1")
			end),

…you get…

1> test_text:run().
....F.
-"This will fail" failed:
        Called erunit:fail/1
fail
2>

To show an error, this is the test:

			test("This will have an error", fun() ->
				erunit:assert("This method does not work with a integer", wrap(1))
			end),

…which produces…

1> test_text:run().
....E.
-"This will have an error" had error:
        error:function_clause
                {erunit,'-test_process/3-fun-0-',3}
fail
2>

…Not the best error message, but I’m working on it. :-)

4 Comments »

  1. anon said,

    May 23, 2007 at 5:01 am

    seen this? :) http://mirroronthenet.org/blog/2007/04/erlunit-unit-testing-framework-for.html

  2. Richard Carlsson said,

    May 23, 2007 at 9:43 am

    Hi! I thought I’d address a couple of the points you were not happy about with EUnit:

    - Tutorial: Well, at least the documentation has been much improved lately - see http://svn.process-one.net/contribs/trunk/eunit/doc/index.html

    - Tests in separate modules: Lately, I’ve added an implicit check for the existence of a module ‘m_tests’ if you tell EUnit to test module ‘m’. If found, all the tests in that module will be run as well. (Note, though, that tests placed in a separate module can’t test functions that are not exported, so you’ll always need the ability to have tests in the same module as the code.)

    - Pattern matching tests: There are several good macros that can/should be used in preference to plain ‘P = Test’ pattern matching: both boolean asserts, the assertMatch macro, and assertions for checking exceptions. All of these give you much better error messages. (I’m not sure what you mean when you write “don’t like using macros specifically for assertions as it makes the failures hard to debug”, since the macros are there specifically to make good error reporting possible. Another thing with the EUnit macros is that they keep working even if the EUnit library modules are not available.)

    Anyway, I don’t want to discourage you from what you do. Your idea of using a behaviour declaration was interesting, and might be something that I’ll add to EUnit; if nothing else, it can work as a marker that shows that the module was compiled with unit testing enabled.

    Good luck with your hacking. You’re very welcome with more input on how EUnit could be improved.

    /Richard

  3. BestFriendChris said,

    May 23, 2007 at 3:29 pm

    Richard Carlsson said,

    - Tutorial: Well, at least the documentation has been much improved lately - see http://svn.process-one.net/contribs/trunk/eunit/doc/index.html

    Thanks. That actually helps quite a bit. :-)

    - Tests in separate modules: Lately, I’ve added an implicit check for the existence of a module ‘m_tests’ if you tell EUnit to test module ‘m’. If found, all the tests in that module will be run as well. (Note, though, that tests placed in a separate module can’t test functions that are not exported, so you’ll always need the ability to have tests in the same module as the code.)

    Great minds think alike, I guess. The only thing I’d like to add is I personally believe in white box testing. If your module has so much going on that you need to test a bunch of private functions, I take that as a tip that I need to refactor my design. That being said, I mostly program in Object Oriented languages, so I don’t have much experience bringing that to a functional point of view. Until I see something that changes that, though, I’m sticking with it. I do like that EUnit now supports what I’m saying, at least.

    - Pattern matching tests: There are several good macros that can/should be used in preference to plain ‘P = Test’ pattern matching: both boolean asserts, the assertMatch macro, and assertions for checking exceptions. All of these give you much better error messages. (I’m not sure what you mean when you write “don’t like using macros specifically for assertions as it makes the failures hard to debug”, since the macros are there specifically to make good error reporting possible. Another thing with the EUnit macros is that they keep working even if the EUnit library modules are not available.)

    To be honest, that was probably a crap statement to make. I wasn’t even commenting about EUnit, actually. What I SHOULD have said was when trying to use macros to test that my assertions failed correctly, I found THOSE macros hard to debug. I actually like the error messages EUnit provides. Method name and line number. Hard to go wrong, really.

    Good luck with your hacking. You’re very welcome with more input on how EUnit could be improved.

    Thanks. Now that I know where the manual is, I’ll take a second look.

  4. Think in Read » links for 2007-05-24 said,

    May 23, 2007 at 7:23 pm

    […] EUnit vs ErUnit @ BestFriendChris (tags: erlang) 归类于: daily del.licio.us — rainhanket @ 9:23 am […]

Leave a Comment