05.11.07
EUnit vs ErUnit
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
- 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.
- 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…
- 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.
- 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…
- 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.
