Erlang学习: EUnit Testing for gen_fsm,erlangeunit


背景:gen_fsm 是Erlang的有限状态机behavior,非常有用。爱立信的一位TDD大神写了一篇如何测试gen_fsm,这个fsm是一个交易系统,负责简单的交易员登陆,插入item,删除item等等,翻译如下:


1. Start and Stop

先看下最初版本的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 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.

译者注:在eunit中, setup返回的值作为所有函数包括cleanup的输入,这里是Pid。started_properly函数是assert 初始为pending, State的值全为空。

现在Test 还不能run,因为tradepost:introspection_statename(Pid) 和 tradepost:introspection_loopdata(Pid)这两个函数还没有。


于是在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}.

这样就可以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.
ok
2>

2. 加入测试用例(identify_seller, insert_item, withdraw_item)

identify_seller seller是登陆函数, insert_item, withdraw_item是增加,删除item的函数

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

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

在tradepost.erl增加相应的函数:

%%-------------------------------------------------------------------
%%% @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}.

再run tests:

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>

3. 使用eunit_fsm

eunit_fsm是作者写的一个module,使gen_fsm的测试看起来更美观:

原来版本:

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


新版本:

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

再看insert_item, 原来版本:

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.

新版本:

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

看起来更易读了吧!


来看下整个的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 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]}
       ]).

在这里我们看下作者自己写的 eunit_fsm.hrl 和  eunit_fsm.erl

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

看下现在的目录结构:

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

4 directories, 4 files

来编译后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>

全Pass!





相关内容