2012-09-05

What to use for testing?

Eventually, rebar (demo video) may be interesting, but I’d like to have a little more meat to my bones first.

At the other extreme, Joe Armstrong wrote about his framework-free unit testing approach.

Let’s go with the standard solution, EUnit.

But wait, there’s more. We also need to decide whether to embed the tests in the module or create a test module. To keep it simple, I’ll drop it in the module (you’re shocked, I’m sure).

If you’ve looked at the code on GitHub, you may have noticed that to date I have yet to limit the exports from this module. That will happen eventually; fortunately, EUnit automatically handles test function exports.

Getting up and running

  1. Add -include_lib("eunit/include/eunit.hrl"). to the top of my module.
  2. Define a function ending with _test.
  3. Compile and run piece:test()
    • EUnit automatically defines test/0 to invoke all of the _test functions.

Believe it or not, that’s all we have to do. Sweet.

For the test function to succeed, it has to return any value. Any other behavior (exception or timeout) is a failure.

Testing the unit testing

First, a simple test:

np_diag1_test() ->
    [ { 3, 4 } ] = next_point( { 4, 5 }, { 2, 3 } ).

Run it:

31> piece:test().
  Test passed.
ok

Simple enough, but it would be good to know that it’s not just blowing smoke. We’ll add a test designed to fail:

diag_1_fail_test() ->
    [ { 4, 4 } ] = next_point( { 4, 5 }, { 2, 3 } ).

Run it:

2> piece:test().
piece: diag_1_fail_test...*failed*
::{badmatch,[{3,4}]}


=======================================================
  Failed: 1.  Skipped: 0.  Passed: 1.
error

I think it’s safe to say that this works.

Now the slog

Unfortunately, learning how to use it (all of 2-3 minutes) was the fun part. Now comes the hard slog of creating a comprehensive test suite.

Fortunately, I can proactively eliminate some of the more complex testing. These are unit tests, after all, so I’ll declare creating a separate process to send messages to a piece process and verify I receive messages back out of scope. I’ll worry about functional tests later. Probably.

I could similarly wave my hands in the air and say I don’t have to test the move functions because they’ll ultimately end up in a different module, but I’ll not do that; after all, I can just move the unit tests with them.

So I should test:

Pardon me if I choose not to list all the permutations on the remaining function calls. I’m already boring myself to tears.

I won’t test validate_diag_slope/1 because there’s absolutely no logic, just the world’s simplest pattern matching behavior.

Oy vey

While writing the tests, I discovered that I swapped my rows and my columns. When defining column_next_point/4, I kept Y as a constant while varying X. Talk about missing the point.1

Fortunately, I likely only would have discovered this if I had a chess piece that could move only in rows or only in columns. Pawns don’t count because their movements are both more and less constrained than that.

I suppose this is why we write tests.

Thankfully, fixing it is as easy as renaming the functions, otherwise I’m sure I’d mess that up too.2

Update: Of course it wasn’t quite that trivial: I also had to update next_point because my logic was screwy there, too. Again, tests: good. Lack of tests: bad. Programmer with awful visualization skills: terrible.

Primitives tests

np_diag1_test() ->
    [ { 3, 4 } ] = next_point( { 4, 5 }, { 2, 3 } ).
np_diag2_test() ->
    [ { 3, 4 }, { 2, 5 } ] = next_point( { 4, 3 }, { 1, 6 } ).
np_col1_test() ->
    [] = next_point( { 0, 0 }, { 0, -1 } ).
np_col2_test() ->
    [ {-3, -6 } ] = next_point( { -3, -7 }, { -3, -5 } ).
np_row1_test() ->
    [ { 1, 5 }, { 2, 5 }, { 3, 5 } ] = next_point( { 0, 5 }, { 4, 5 } ).

row_1_test() ->
    [ { -42, 15 }, { -41, 15 } ] = row_next_point(15, -43, -40, []).
row_2_test() ->
    [] = row_next_point(3, 4, 3, []).

col_1_test() ->
    [ { 8, 1 }, { 8, 2 }, { 8, 3 } ] = column_next_point(8, 0, 4, []).

slope_1_test() ->
    infinity = calculate_slope( { 3, 5 }, { 3, -14 } ).
slope_2_test() ->
    none = calculate_slope( { 3, 5 }, { 3, 5 } ).
slope_3_test() ->
    0.0 = calculate_slope( { 3, 5 }, { 99, 5 } ).
slope_4_test() ->
    -1.0 = calculate_slope( { 3, 5 }, { 8, 0 } ).
slope_5_test() ->
    1.0 = calculate_slope( { 3, 5 }, { -2, 0 } ).

Movefuns tests

queen_nomove_test() ->
    [ Q ] = [ X || {queenmove, X} <- movefuns() ],
    { {3, 3}, [] } = Q( {3, 3}, {5, 6}, white ).

queen_diag_test() ->
    [ Q ] = [ X || {queenmove, X} <- movefuns() ],
    { {6, 42}, [ { 8, 40 }, { 7, 41 } ] } = Q( { 9, 39 }, { 6, 42 }, black ).

queen_col_test() ->
    [ Q ] = [ X || {queenmove, X} <- movefuns() ],
    { {6, 42}, [ { 6, 40 }, { 6, 41 } ] } = Q( { 6, 39 }, { 6, 42 }, black ).

rook_col_test() ->
    [ R ] = [ X || {rookmove, X} <- movefuns() ],
    { {2, 35}, [ { 2, 36 } ] } = R( { 2, 37 }, { 2, 35 }, black ).

rook_row_test() ->
    [ R ] = [ X || {rookmove, X} <- movefuns() ],
    { {2, 35}, [ { 3, 35 } ] } = R( { 4, 35 }, { 2, 35 }, black ).

pawn_nocap_test() ->
    [ Pc ] = [ X || {pawncap, X} <- movefuns() ],
    { {3, 4}, [] } = Pc({3, 4}, {3, 5}, white).

pawn_cap1_test() ->
    [ Pc ] = [ X || {pawncap, X} <- movefuns() ],
    { {4, 5}, [] } = Pc({3, 4}, {4, 5}, white).

pawn_cap2_test() ->
    [ Pc ] = [ X || {pawncap, X} <- movefuns() ],
    { {4, 5}, [] } = Pc({5, 6}, {4, 5}, black).

pawn_move1_test() ->
    [ Pm ] = [ X || {pawnmove, X} <- movefuns() ],
    { {4, 4}, [{4, 3}] } = Pm({4, 2}, {4, 4}, white).

pawn_move2_test() ->
    [ Pm ] = [ X || {pawnmove, X} <- movefuns() ],
    { {4, 2}, [] } = Pm({4, 2}, {4, 4}, black).

pawn_move3_test() ->
    [ Pm ] = [ X || {pawnmove, X} <- movefuns() ],
    { {4, 4}, [] } = Pm({4, 3}, {4, 4}, white).

pawn_move4_test() ->
    [ Pm ] = [ X || {pawnmove, X} <- movefuns() ],
    { {4, 4}, [] } = Pm({4, 5}, {4, 4}, black).

Names could be both more consistent and more documentary in nature, but all 25 of them pass. You don’t want to know how many of them failed initially because I couldn’t write a proper test.

Now I just have to remember to call them periodically, which I’m sure rebar could help me with. Baby steps.


  1. Get it? Missing the point? I slay me.

  2. And yes, I did originally start modifying the function internals before realizing what an idiot I was. Sosumi.

erlang chessboard unit testing
Previous post
Chessboard: Day 6 8th grade Algebra defeats me.
Next post
Chessboard: Day 8 Board yet?