背景: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 <[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}). %%% 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!