Post: Eunit testing with Erlang and Chicago Boss

by @jgordor, 02 Feb 2013.

Architecture introduction

First we need to know how a Chicago Boss application works and the differences with a normal OTP app. When the application is fully compiled, it's behaves as a normal OTP app, but their internals have a few peculiarities (very short and incomplete story):

  • Compilation: Controllers and models are special parametrized erlang modules, the compiler chain handles the magic and adds useful functions that helps application code reduction.
  • Directory structure: Chicago Boss has a well defined directory structure to separate MVC (model, view, controller) and libs.
  • Multiapp setups: Chicago Boss apps can be mounted as lego pieces, allowing a great separation and reusability of code into functional units (ex: app_core, app_hr, app_accounting, ...) running in the same erlang vm sharing logic & view code.
  • Boot: Chicago Boss implements custom init modules located on priv/init that helps you run initialization code on the boot and shutdown process.

Overview of what can be done now()

It's important to know that depending on your testing needs, you can think that the actual implementation is overkill. If you only need to test a plain erlang module (lib), the default rebar eunit implementation is enough. But if you need (by example) a complete test of a model, before you can run normal eunit tests you need the application fully initialized, boss_db & boss_news services started, all dependent apps initialized, ...

The tests commands (actually eunit & functional) loads the configuration from boss.test.config, allowing separate the test environment (db, sessions, ...) from development or production. Simply put your eunit tests modules on src/tests/eunit and you can launch this from command line (all or specific suites):

# Run all eunit tests
./rebar boss c=test_eunit
# Run only a specific test
./rebar boss c=test_eunit suite=mymodel_tests

A complete step by step example

Objective: Test a model called "account" where we will store user account data. We will add some validation and custom functions plus a before_create/before_update model hook that will maintain the created_at and updated_at fields automatically.

The "account" model fields: Id, FirstName, LastName, CreatedAt and UpdatedAt.

Database adapter: Should work with all database adapters, for this how to we will use the internal mock database system shipped with the framework itself

Setting up Chicago Boss and the barebone application

# You need erlang installed, on ubuntu simply
sudo apt-get install erlang
# We work with the latest code, so, install git
sudo apt-get install git-core
# Clone and compile the Chicago Boss framework repository
git clone git://github.com/evanmiller/ChicagoBoss.git
cd ChicagoBoss
make
# Create the application, we named it ex_eunit
make app PROJECT=ex_eunit
cd ../ex_eunit

Now you have the project ready for development, time to add the account model, create a file named src/model/account.erl

-module(account,
        [Id, 
         FirstName::string(), 
         LastName::string(), 
         CreatedAt::datetime(), 
         UpdatedAt::datetime()
        ]).
-compile(export_all).

%%%============================================================================
%%% API
%%%============================================================================

full_name() ->
    FirstName ++ " " ++ LastName.

%%%============================================================================
%%% Model Validations
%%%============================================================================

validation_tests() ->
    [
     %% FirstName Required
     {fun() ->
              FirstName =/= undefined andalso
                  length(FirstName) =/= 0
      end, {first_name, "Required"}},
     %% LastName Required
     {fun() ->
              LastName =/= undefined andalso
                  length(LastName) =/= 0
      end, {last_name, "Required"}}
   ].

%%%============================================================================
%%% Model Hooks
%%%============================================================================

%% Set created_at and updated_at on creation
before_create() ->
    Now = calendar:now_to_universal_time(erlang:now()),
    {ok, set([{created_at, Now}, {updated_at, Now}])}.

%% Update updated_at on update
before_update() ->
  {ok, set(updated_at, calendar:now_to_universal_time(erlang:now()))}.

The model code is complete, you can boot the cb app (./init-dev.sh) and start interacting from the erlang shell

(ex_eunit@erlyhub-dev)1> boss_db:find(account, []). 
[]
(ex_eunit@erlyhub-dev)2> A1 = boss_record:new(account, [{first_name, "Jose"}]).
{account,id,"Jose",undefined,undefined,undefined}
(ex_eunit@erlyhub-dev)3> A1:save().
{error,[{last_name,"Required"}]}
(ex_eunit@erlyhub-dev)4> A2 = A1:set(last_name, "Gordo").
{account,id,"Jose","Gordo",undefined,undefined}
(ex_eunit@erlyhub-dev)5> A2:save().
{ok,{account,"account-1","Jose","Gordo",
             {{2013,2,10},{17,2,9}},
             {{2013,2,10},{17,2,9}}}}
(ex_eunit@erlyhub-dev)6> boss_db:find(account, []).
[{account,"account-1","Jose","Gordo",
          {{2013,2,10},{17,2,9}},
          {{2013,2,10},{17,2,9}}}]

Ok, seems that is working, now will make a complete eunit test suite, create a file named src/test/eunit/account_tests.erl

-module(account_tests).
-include_lib("eunit/include/eunit.hrl").

%%%============================================================================
%%% API
%%%============================================================================

suite_test_()->
    Suite = 
    {foreach, local,
      fun setup/0,
      tests()
     },
    Suite.

tests() ->
    [        
     {"Validations of first_name & last_name",
      ?_test(val_first_and_last_name())},
     {"Validations of full_name",
      ?_test(val_full_name())},
     {"Validations of generated_dates",
      ?_test(val_gen_dates())}
    ].

val_first_and_last_name() ->
    Account = boss_record:new(account, []),
    %% First/Last Name is required
    ?assertEqual({error, [{first_name, "Required"}, {last_name, "Required"}]}, Account:save()),
    Account2 = Account:set([{first_name, "John"}, {last_name, "Dinho"}]),
    {Res, _} = Account2:save(),
    ?assertEqual(ok, Res).

val_full_name() ->
    Account = boss_record:new(account, [{first_name, "John"}, {last_name, "Dinho"}]),
    ?assertEqual("John Dinho", Account:full_name()).

val_gen_dates() ->
    Account = boss_record:new(account, [{first_name, "John"}, {last_name, "Dinho"}]),
    {ok, SavedAccount} = Account:save(),
    ?assertNotEqual(undefined, SavedAccount:created_at()),
    ?assertNotEqual(undefined, SavedAccount:updated_at()).
    

%% ===================================================================
%% Internal functions
%% ===================================================================

%%--------------------------------------------------------------------
%% @doc Setup each test set
%%--------------------------------------------------------------------
setup()->
    ok.

Create the boss.test.config, for now, copy the default boss.config:

cp boss.config boss.test.config

And finally, run the test!

./rebar boss c=test_eunit
....
  All 3 tests passed.

That's all, you're ready to test your code, and see how fast it is if you come from rails

Some advanced tips

Finally, I will introduce Meck, a mocking library for Erlang that will make your life easier if you need to test time-dependent functionality, or you are using external elements in your app:

With meck you can easily mock modules in Erlang. You can also perform some basic validations on the mocked modules, such as making sure no unexpected exceptions occurred or looking at the call history.

Is already required by a Chicago Boss dependency (from 0.8), so, you can start using it, if for some reason you get "undefined" calling the meck module, put this line in your rebar.config:

{deps, [
    {meck, ".*", {git, "git://github.com/eproxus/meck.git", "8433cf2e07"}}
  ]}.

and execute from command line:

./rebar get-deps compile

Important!

To preserve your mental health, put this on your boss.test.config, inside the boss section:

            {compiler_options, [debug_info]}
        

The Chicago Boss compiler will add the debug information to the modules required for debugging, meck [unstick, passthrough], ...

Important 2.0!

If you are using R15B or R15B01, eunit will not output exception stack traces when running your eunit tests, this can cause a lot of pain, you can apply this patch.


As a final note, the debug_info compiler option introduced in 0.8 opens up the possibility to restore the code coverage in eunit, but this will be covered in another post.

Comments or improvements welcome!

Share on
comments powered by Disqus