Erlang learning: eunit testing for gen_fsm

Source: Internet
Author: User

Background: gen_fsm is a finite state machine behavior of Erlang, which is very useful. One of Ericsson's TDD experts wrote an article on how to test gen_fsm. This FSM is a trading system responsible for simple traders to log on, insert items, delete items, etc:


1. Start and Stop

First take a look at the original version of tradepost_tests:

-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 testsetmain_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 Cleanupsetup()      -> {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 thisstarted_properly(Pid) ->    fun() ->            ?assertEqual(pending,tradepost:introspection_statename(Pid)),            ?assertEqual([undefined,undefined,undefined,undefined,undefined],                         tradepost:introspection_loopdata(Pid))    end.

Note: In eunit, the value returned by setup serves as the input for all functions, including cleanup. Here is the PID. The started_properly function is assert initially pending, and the state value is empty.

Currently, test cannot run because tradepost: introspection_statename (PID) and tradepost: introspection_loopdata (PID) functions are not available.


Therefore, add the following to tradepost. erl:

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).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}.

In this way, you can run test.

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.ok2>

2. Add test cases (identify_seller, insert_item, withdraw_item)

Identify_seller is the login function, Insert_item, withdraw_item is the function for adding and deleting items.

% This is the main point of "entry" for my EUnit testing.% A generator which forces setup and cleanup for each test in the testsetmain_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     ]}.% Now, we are adding the Seller API testsidentify_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.

Add the corresponding function in tradepost. erl:

%%-------------------------------------------------------------------%%% @author Gianfranco <[email protected]>%%% @copyright (C) 2010, Gianfranco%%% Created :  2 Sep 2010 by Gianfranco <[email protected]>%%%--------------------------------------------------------------------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}).%%% APIstart_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}.

Run tests again:

zen:EUnitFSM zenon$ erlc -o ebin/ src/*.erl test/*.erlzen: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>

3. Use eunit_fsm

Eunit_fsm is a module written by the author to make gen_fsm testing look more beautiful:

Original version:

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


New Version:

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

Let's look at insert_item, the original version:

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.

New Version:

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]}      ]}.

It looks easier to read!


Let's take a look at tradepost_test.erl.

-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 testsetmain_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 Cleanupsetup()      -> {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 thisstarted_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 testsidentify_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]}       ]).

Here, let's take a look at the eunit_fsm.hrl and eunit_fsm.erl written by the author.

Eunit_fsm.hrl:

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

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 StateDataget(Id,Which) ->    {status,_Pid,_ModTpl, List} = sys:get_status(Id),    AllData = lists:flatten([ X || {data,X} <- lists:last(List) ]),    proplists:get_value(Which,AllData).

Check the current directory structure:

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

After compilation, run:

zen:EUnitFSM zenon$ erlc -o ebin/ src/*.erl test/*.erlzen: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>

Full pass!


Erlang learning: eunit testing for gen_fsm

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.