EUnit Testing gen_fsm – Part 1


This post will be TDD based and shows how the Tradepost program is developed and is tested.

First iteration – Start and Stop

I start writing the test! This gives me the opportunity to crystallize my thoughts about the program.

The module implementing the tests of the Tradepost

-module(tradepost_tests).
-include_lib("eunit/include/eunit.hrl").
% This is the main point of "entry" for my EUnit testing.
% A generator which forces setup and cleanup for each test in the testset
main_test_() ->
    {foreach,
     fun setup/0,
     fun cleanup/1,
     % Note that this must be a List of TestSet or Instantiator
     % (I have instantiators == functions generating tests)
     [
      % First Iteration
      fun started_properly/1,
     ]}.

% Setup and Cleanup
setup()      -> {ok,Pid} = tradepost:start_link(), Pid.
cleanup(Pid) -> tradepost:stop(Pid).

% Pure tests below
% ------------------------------------------------------------------------------
% Let's start simple, I want it to start and check that it is okay.
% I will use the introspective function for this
started_properly(Pid) ->
    fun() ->
            ?assertEqual(pending,tradepost:introspection_statename(Pid)),
            ?assertEqual([undefined,undefined,undefined,undefined,undefined],
                         tradepost:introspection_loopdata(Pid))
    end.

Compilation and running should fail as the gen_fsm module is barely minimal

zen:EUnitFSM zenon$ tree .
.
├── ebin
├── include
├── src
│   └── tradepost.erl
└── test
    └── tradepost_tests.erl

4 directories, 2 files
zen:EUnitFSM zenon$ 

zen:EUnitFSM zenon$ erlc -o ebin/ src/*.erl test/*.erl
zen:EUnitFSM zenon$ erl -pa ebin/
Erlang R13B04 (erts-5.7.5) [source] [64-bit] [smp:4:4] [rq:4]
[async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.7.5  (abort with ^G)
1> eunit:test(tradepost).
tradepost_tests: started_properly...*failed*
::error:{assertEqual_failed,
          [{module,tradepost_tests},
           {line,33},
           {expression,"tradepost : introspection_statename ( Pid )"},
           {expected,pending},
           {value,ok}]}
  in function tradepost_tests:'-started_properly/1-fun-0-'/2
  in call from tradepost_tests:'-started_properly/1-fun-2-'/1

module 'tradepost_tests'
*** context cleanup failed ***
::error:undef
  in function tradepost:stop/1
    called as stop(<0.39.0>)

=======================================================
  Failed: 1.  Skipped: 0.  Passed: 0.
One or more tests were cancelled.
error
2>

Clearly the tradepost does not handle the introspective functions and stop, let’s write them first.

The interface

introspection_statename(TradePost) ->
    gen_fsm:sync_send_all_state_event(TradePost,which_statename).
introspection_loopdata(TradePost) ->
    gen_fsm:sync_send_all_state_event(TradePost,which_loopdata).
stop(Pid) -> gen_fsm:sync_send_all_state_event(Pid,stop).

And the handling

handle_sync_event(which_statename, _From, StateName, LoopData) ->
    {reply, StateName, StateName, LoopData};
handle_sync_event(which_loopdata, _From, StateName, LoopData) ->
    {reply,tl(tuple_to_list(LoopData)),StateName,LoopData};
handle_sync_event(stop,_From,_StateName,LoopData) ->
    {stop,normal,ok,LoopData}.

Running

zen:EUnitFSM zenon$ erl -pa ebin/
Erlang R13B04 (erts-5.7.5) [source] [64-bit] [smp:4:4] [rq:4]
[async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.7.5  (abort with ^G)
1> eunit:test(tradepost,[verbose]).
======================== EUnit ========================
module 'tradepost'
  module 'tradepost_tests'
    tradepost_tests: started_properly...ok
    [done in 0.004 s]
  [done in 0.005 s]
=======================================================
  Test passed.
ok
2>


Wow, I’m glad we got that sorted out. Now,  as we have set the first nail in the mountain and hooked us to it, the climb begins in a series of cycles. Next up, the Seller API.

Second Iteration – Seller API

What the seller needs, is a way to insert an item, and a way to remove an item. Also to identify him/her self with a naive password approach. Once identified, the seller (and only the seller) should be able to add and retract items.

The main_test_() has now been expanded to

% This is the main point of "entry" for my EUnit testing.
% A generator which forces setup and cleanup for each test in the testset
main_test_() ->
    {foreach,
     fun setup/0,
     fun cleanup/1,
     % Note that this must be a List of TestSet or Instantiator
     % (I have instantiators)
     [
      % First Iteration
      fun started_properly/1,
      % Second Iteration
      fun identify_seller/1,
      fun insert_item/1,
      fun withdraw_item/1
     ]}.

And the definition of the tests themselves

% Now, we are adding the Seller API tests
identify_seller(Pid) ->
    fun() ->
            % From Pending, identify seller, then state should be pending
            % loopdata should now contain seller_password
            ?assertEqual(pending,tradepost:introspection_statename(Pid)),
            ?assertEqual(ok,tradepost:seller_identify(Pid,seller_password)),
            ?assertEqual(pending,tradepost:introspection_statename(Pid)),
            ?assertEqual([undefined,undefined,seller_password,undefined,
                       undefined],tradepost:introspection_loopdata(Pid))
    end.

insert_item(Pid) ->
    fun() ->
            % From pending and identified seller, insert item
            % state should now be item_received, loopdata should now contain itm
            tradepost:introspection_statename(Pid),
            tradepost:seller_identify(Pid,seller_password),
            ?assertEqual(ok,tradepost:seller_insertitem(Pid,playstation,
                                                  seller_password)),
            ?assertEqual(item_received,tradepost:introspection_statename(Pid)),
            ?assertEqual([playstation,undefined,seller_password,undefined,
                       undefined],tradepost:introspection_loopdata(Pid))
    end.

withdraw_item(Pid) ->
    fun() ->
            % identified seller and inserted item, withdraw item
            % state should now be pending, loopdata should now contain only password
            tradepost:seller_identify(Pid,seller_password),
            tradepost:seller_insertitem(Pid,playstation,seller_password),
            ?assertEqual(ok,tradepost:withdraw_item(Pid,seller_password)),
            ?assertEqual(pending,tradepost:introspection_statename(Pid)),
            ?assertEqual([undefined,undefined,seller_password,undefined,
                       undefined],tradepost:introspection_loopdata(Pid))
    end.

Now, this is a lot of code duplication per test, and  I shall start rewriting the code into something more wieldy, but first: the failed compilation and the code making it pass.

zen:EUnitFSM zenon$ erlc -o ebin/ src/*.erl test/*.erl
zen:EUnitFSM zenon$ erl -pa ebin/
Erlang R13B04 (erts-5.7.5) [source] [64-bit] [smp:4:4] [rq:4]
[async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.7.5  (abort with ^G)
1> eunit:test(tradepost).
tradepost_tests: identify_seller...*failed*
::error:undef
  in function tradepost:seller_identify/2
    called as seller_identify(<0.39.0>,seller_password)
  in call from tradepost_tests:'-identify_seller/1-fun-1-'/2
  in call from tradepost_tests:'-identify_seller/1-fun-4-'/1

tradepost_tests: insert_item...*failed*
::error:undef
  in function tradepost:seller_identify/2
    called as seller_identify(<0.39.0>,seller_password)
  in call from tradepost_tests:'-insert_item/1-fun-3-'/1

tradepost_tests: withdraw_item...*failed*
::error:undef
  in function tradepost:seller_identify/2
    called as seller_identify(<0.39.0>,seller_password)
  in call from tradepost_tests:'-withdraw_item/1-fun-3-'/1

=======================================================
  Failed: 3.  Skipped: 0.  Passed: 1.
error
2>

Some writing and enjoying later, the module

%%%-------------------------------------------------------------------
%%% @author Gianfranco <zenon@zen.home>
%%% @copyright (C) 2010, Gianfranco
%%% Created :  2 Sep 2010 by Gianfranco <zenon@zen.home>
%%%-------------------------------------------------------------------
-module(tradepost).
-behaviour(gen_fsm).

%% API
-export([start_link/0,introspection_statename/1,introspection_loopdata/1,
         stop/1,seller_identify/2,seller_insertitem/3,withdraw_item/2]).

%% States
-export([pending/2,pending/3,item_received/3]).

%% gen_fsm callbacks
-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3,
         terminate/3, code_change/4]).
-record(state, {object,cash,seller,buyer,time}).

%%% API
start_link() -> gen_fsm:start_link(?MODULE, [], []).

introspection_statename(TradePost) ->
    gen_fsm:sync_send_all_state_event(TradePost,which_statename).
introspection_loopdata(TradePost) ->
    gen_fsm:sync_send_all_state_event(TradePost,which_loopdata).
stop(Pid) -> gen_fsm:sync_send_all_state_event(Pid,stop).

seller_identify(TradePost,Password) ->
    gen_fsm:sync_send_event(TradePost,{identify_seller,Password}).
seller_insertitem(TradePost,Item,Password) ->
    gen_fsm:sync_send_event(TradePost,{insert,Item,Password}).

withdraw_item(TradePost,Password) ->
    gen_fsm:sync_send_event(TradePost,{withdraw,Password}).

%%--------------------------------------------------------------------
pending(_Event,LoopData) -> {next_state,pending,LoopData}.

pending({identify_seller,Password},_Frm,LoopD = #state{seller=Password}) ->
    {reply,ok,pending,LoopD};
pending({identify_seller,Password},_Frm,LoopD = #state{seller=undefined}) ->
    {reply,ok,pending,LoopD#state{seller=Password}};
pending({identify_seller,_},_,LoopD) ->
    {reply,error,pending,LoopD};

pending({insert,Item,Password},_Frm,LoopD = #state{seller=Password}) ->
    {reply,ok,item_received,LoopD#state{object=Item}};
pending({insert,_,_},_Frm,LoopD) ->
    {reply,error,pending,LoopD}.

item_received({withdraw,Password},_Frm,LoopD = #state{seller=Password}) ->
    {reply,ok,pending,LoopD#state{object=undefined}};
item_received({withdraw,_},_Frm,LoopD) ->
    {reply,error,item_received,LoopD}.

%%--------------------------------------------------------------------
handle_sync_event(which_statename, _From, StateName, LoopData) ->
    {reply, StateName, StateName, LoopData};
handle_sync_event(which_loopdata, _From, StateName, LoopData) ->
    {reply,tl(tuple_to_list(LoopData)),StateName,LoopData};
handle_sync_event(stop,_From,_StateName,LoopData) ->
    {stop,normal,ok,LoopData};
handle_sync_event(_E,_From,StateName,LoopData) ->
    {reply,ok,StateName,LoopData}.

%%--------------------------------------------------------------------
init([]) -> {ok, pending, #state{}}.
handle_event(_Event, StateName, State) ->{next_state, StateName, State}.
handle_info(_Info, StateName, State) -> {next_state, StateName, State}.
terminate(_Reason, _StateName, _State) -> ok.
code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}.

And proof for the functioning of it.

zen:EUnitFSM zenon$ erlc -o ebin/ src/*.erl test/*.erl
zen:EUnitFSM zenon$ erl -pa ebin/ -eval 'eunit:test(tradepost,[verbose]).'
Erlang R13B04 (erts-5.7.5) [source] [64-bit] [smp:4:4] [rq:4]
[async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.7.5  (abort with ^G)
1> ======================== EUnit ========================
module 'tradepost'
  module 'tradepost_tests'
    tradepost_tests: started_properly...ok
    tradepost_tests: identify_seller...ok
    tradepost_tests: insert_item...ok
    tradepost_tests: withdraw_item...ok
    [done in 0.015 s]
  [done in 0.015 s]
=======================================================
  All 4 tests passed.
1>

Awesome! And we got a bonus as well: how to run eunit directly from the command line with the -eval command. Cool, oneliners always make you feel more “1337”.

It all looks very good so far, but I will use the third iteration to fixing up the eunit module, it has way to much duplication and could be made more declarative (specifying what should be computed – not how [ which is kind of more abstract <of course someone will oppose>]).

Third Iteration –  Eunit GenFSM DSL (do not worry)

Testing gen_fsm’s should ideally be all about testing state transitions, in-state computations and variables. For this purpose, I would like to have my DSL that handles all of the pesky details for me.

To exemplify, I would like to write the first test

started_properly(Pid) ->
    fun() ->
            ?assertEqual(pending,tradepost:introspection_statename(Pid)),
            ?assertEqual([undefined,undefined,undefined,undefined,undefined],
                         tradepost:introspection_loopdata(Pid))
    end.

as something along these lines

started_properly(Pid) ->
    {"Proper startup test",
     [{statename,is,pending},
      {loopdata,is,[undefined,undefined,undefined,undefined,undefined]}
      ]}.

And, this test

insert_item(Pid) ->
    fun() ->
        % From pending and identified seller, insert item
        % state should now be item_received, loopdata should now contain itm
        tradepost:introspection_statename(Pid),
        tradepost:seller_identify(Pid,seller_password),
        ?assertEqual(ok,tradepost:seller_insertitem(Pid,playstation,
                                              seller_password)),
        ?assertEqual(item_received,tradepost:introspection_statename(Pid)),
        ?assertEqual([playstation,undefined,seller_password,undefined,
                   undefined],tradepost:introspection_loopdata(Pid))
    end.

as something along these lines

insert_item(Pid) ->
    {"Insert Item Test",
      [{state,is,pending},
       {call,tradepost,seller_identify,[Pid,seller_password],ok},
       {call,tradepost,seller_insertitem,[Pid,playstation,seller_password]},
       {state,is,item_received},
       {loopdata,is,[playstation,undefined,seller_password,undefined,undefined]}
      ]}.

This is a symbolic method that defines our DSL in a yet readable way, hiding the logic. For this to work, we need a translation from our DSL to actual EUnit syntax without losing the EUnit machinery. We will also drop the intrusive introspective functions for the usage of the better sys:get_status/1 (thank you Ulf).

Thus, let the journey begin, first with the translation of the Test. This can be done in two ways, either at runtime or at compile time with parse-transforms. I choose the runtime one with translation functions and macros. Ulf Wiger has a neat library for cooler parse transforms, but I shall not use this for now, (the code will probably be revised many times)

The test module has now been changed to

-module(tradepost_tests).
-include_lib("eunit/include/eunit.hrl").
-include("include/eunit_fsm.hrl").

% This is the main point of "entry" for my EUnit testing.
% A generator which forces setup and cleanup for each test in the testset
main_test_() ->
    {foreach,
     fun setup/0,
     fun cleanup/1,
     % Note that this must be a List of TestSet or Instantiator
     [
      % First Iteration
      fun started_properly/1,
      % Second Iteration
      fun identify_seller/1,
      fun insert_item/1,
      fun withdraw_item/1
     ]}.

% Setup and Cleanup
setup()      -> {ok,Pid} = tradepost:start_link(), Pid.
cleanup(Pid) -> tradepost:stop(Pid).

% Pure tests below
% ------------------------------------------------------------------------------
% Let's start simple, I want it to start and check that it is okay.
% I will use the introspective function for this
started_properly(Pid) ->
    ?fsm_test(tradepost,Pid,"Started Properly Test",
      [{state,is,pending},
       {loopdata,is,[undefined,undefined,undefined,undefined,undefined]}
     ]).

% Now, we are adding the Seller API tests
identify_seller(Pid) ->
    ?fsm_test(Pid,"Identify Seller Test",
      [{state,is,pending},
       {call,tradepost,seller_identify,[Pid,seller_password],ok},
       {state,is,pending},
       {loopdata,is,[undefined,undefined,seller_password,undefined,undefined]}
      ]).

insert_item(Pid) ->
    ?fsm_test(Pid,"Insert Item Test",
       [{state,is,pending},
        {call,tradepost,seller_identify,[Pid,seller_password],ok},
        {call,tradepost,seller_insertitem,[Pid,playstation,seller_password],ok},
        {state,is,item_received},
        {loopdata,is,[playstation,undefined,seller_password,undefined,undefined]}
       ]).

withdraw_item(Pid) ->
    ?fsm_test(Pid,"Withdraw Item Test",
       [{state,is,pending},
        {call,tradepost,seller_identify,[Pid,seller_password],ok},
        {call,tradepost,seller_insertitem,[Pid,button,seller_password],ok},
        {state,is,item_received},
        {call,tradepost,seller_withdraw_item,[Pid,seller_password],ok},
        {state,is,pending},
        {loopdata,is,[undefined,undefined,seller_password,undefined,undefined]}
       ]).

And the supporting logic is found in eunit_fsm.hrl

-define(fsm_test(Id,Title,CmdList),
  {Title,fun() -> [ eunit_fsm:translateCmd(Id,Cmd) || Cmd <- CmdList] end}).

together with eunit_fsm.erl

-module(eunit_fsm).
-export([translateCmd/2,get/2]).
-define(Expr(X),??X).
translateCmd(Id,{state,is,X}) ->
    case get(Id,"StateName") of
        X -> true;
        _V ->  .erlang:error({statename_match_failed,
                              [{module, ?MODULE},
                               {line, ?LINE},
                               {expected, X},
                               {value, _V}]})
    end;
translateCmd(_Id,{call,M,F,A,X}) ->
    case apply(M,F,A) of
        X -> ok;
        _V ->  .erlang:error({function_call_match_failed,
                              [{module, ?MODULE},
                               {line, ?LINE},
                               {expression, ?Expr(apply(M,F,A))},
                               {expected, X},
                               {value, _V}]})
    end;
translateCmd(Id,{loopdata,is,X}) ->
    case tl(tuple_to_list(get(Id,"StateData"))) of
        X    -> true;
        _V ->    .erlang:error({loopdata_match_failed,
                                [{module, ?MODULE},
                                 {line, ?LINE},
                                 {expected, X},
                                 {value, _V}]})
    end.

% StateName or StateData
get(Id,Which) ->
    {status,_Pid,_ModTpl, List} = sys:get_status(Id),
    AllData = lists:flatten([ X || {data,X} <- lists:last(List) ]),
    proplists:get_value(Which,AllData).

Now, we have a method of expressing the tests more clearly, next up is the buyer API. Testing if of the essence!

zen:EUnitFSM zenon$ tree .
.
├── ebin
├── include
│   └── eunit_fsm.hrl
├── src
│   └── tradepost.erl
└── test
    ├── eunit_fsm.erl
    └── tradepost_tests.erl

4 directories, 4 files
zen:EUnitFSM zenon$ 

Compile and run

zen:EUnitFSM zenon$ erlc -o ebin/ src/*.erl test/*.erl
zen:EUnitFSM zenon$ erl -pa ebin/ -eval 'eunit:test(tradepost,[verbose]).'
Erlang R13B04 (erts-5.7.5) [source] [64-bit] [smp:4:4] [rq:4]
[async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.7.5  (abort with ^G)
1> ======================== EUnit ========================
module 'tradepost'
  module 'tradepost_tests'
    tradepost_tests: started_properly (Started Properly Test)...[0.001 s] ok
    tradepost_tests: identify_seller (Identify Seller Test)...ok
    tradepost_tests: insert_item (Insert Item Test)...ok
    tradepost_tests: withdraw_item (Withdraw Item Test)...ok
    [done in 0.014 s]
  [done in 0.014 s]
=======================================================
  All 4 tests passed.

1>

And so, it works. Next part is adding buyer API and extending the DSL where needed, also maybe rewriting it with Ulf’s awesome parse transform libs.

About these ads
Leave a comment

6 Comments

  1. Your very first code example div has an extra comma that will cause the erlang compiler to fail:

    it appears as:

    [
    % First Iteration
    fun started_properly/1,
    ]}.

    but should be:

    [
    % First Iteration
    fun started_properly/1
    ]}.

    Also, the definition for the start_properly/1 test with the ?fsm_test DSL is passed 4 argument and not 3 as defined in the marco in eunit_fsm.hrl.

    This is an erlang compile error:

    started_properly(Pid) ->
    ?fsm_test(tradepost,Pid,”Started Properly Test”,
    [{state,is,pending},
    {loopdata,is,[undefined,undefined,undefined,undefined,undefined]}
    ]).

    it appears from the other tests that it should not be passing the atom ‘tradepost’, and should appear like so:

    started_properly(Pid) ->
    ?fsm_test(Pid,”Started Properly Test”,
    [{state,is,pending},
    {loopdata,is,[undefined,undefined,undefined,undefined,undefined]}
    ]).

    there is also an error is one of the method atoms that is being specified in the DSL, it’s in this test:

    withdraw_item(Pid) ->
    ?fsm_test(Pid,”Withdraw Item Test”,
    [{state,is,pending},
    {call,tradepost,seller_identify,[Pid,seller_password],ok},
    {call,tradepost,seller_insertitem,[Pid,button,seller_password],ok},
    {state,is,item_received},
    {call,tradepost,seller_withdraw_item,[Pid,seller_password],ok},
    {state,is,pending},
    {loopdata,is,[undefined,undefined,seller_password,undefined,undefined]}
    ]).

    the issue is in the {call,tradepost,seller_withdraw_item…} tuple, it should not be ‘seller_withdraw_item’, it should be ‘withdraw_item’, like so

    withdraw_item(Pid) ->
    ?fsm_test(Pid,”Withdraw Item Test”,
    [{state,is,pending},
    {call,tradepost,seller_identify,[Pid,seller_password],ok},
    {call,tradepost,seller_insertitem,[Pid,button,seller_password],ok},
    {state,is,item_received},
    {call,tradepost,withdraw_item,[Pid,seller_password],ok},
    {state,is,pending},
    {loopdata,is,[undefined,undefined,seller_password,undefined,undefined]}
    ]).

    Then all the tests will pass…

    Reply
  2. Also – it would be helpful to have a view of the minimal get_fsm behavior for the tradepost.erl if you’re a green erlang programmer like me it wasn’t 100% clear what you were looking for. I just added methods until it compiled from the module that you display later. But from a “follow along” perspective, it was a speed bump for me.

    Reply
    • Yeah, something that probably all posts are suffering from, is that this was the first time that I posted code to a blog.
      As you notice, more than one time, I got into the situation that the code is partial/wrongly formatted.

      After this, I kind of stopped blogging, and just put stuff to github publicly for people to read – this way I can guarantee that
      things work.

      I hope that it sparked some interest for you in some way.

      Cheers

      Reply
      • Gianfranco – it definitely sparked some interest and it actually was good for me to troubleshoot the problems because I’m never to Erlang. Thanks for the posts.

        Writing step-by-step instructions for tutorials is hard. I used to teach and it was always far more challenging they people would imagine to do handouts and other materials. Doing Git commits at each step is helpful but if you’re trailblazing new code, it can be hard to get right code snapshots captured if you’re exploring what’s “the right code” in the first place.

  3. Actually, do you happen to have any suggestions for good tools that can facilitate this greatly? Like some interface/tools that can easily link and format code from a git repo and then embed it onto a page?
    This would greatly facilitate such future endeavors!

    Reply
    • Gianfranco – I think there is a WordPress plugin for included GitHub Gists in blog entries:

      http://wordpress.org/plugins/embed-github-gist/

      So that would be one way for you to include code that you could edit and version for your posts. There are also lots of people running their blogs off of GitHub repos with Jekyll. That might make versions & editing a bit easier too. I suggest the Gist plugin because you’ve already got your WordPress site so that I think that’s a matter was just getting the plugin available.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: