• Erlang学习: EUnit Testing for gen_fsm


    背景: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。


  • 相关阅读:
    python处理yml
    awk命令笔记
    微信小程序wxml的数据传给js 点击事件 js获取view中的内容
    微信小程序js 字符串截取
    微信小程序 wx:if 多条件判断
    微信小程序 --- toast消息提示框
    微信小程序 点击事件获取到的 event.currentTarget.dataset.id 是空的 解决办法
    微信小程序页面跳转传参数
    微信小程序开发框架
    微信小程序获取当前时间
  • 原文地址:https://www.cnblogs.com/lcchuguo/p/5075779.html
Copyright © 2020-2023  润新知