Last active
August 29, 2015 14:07
-
-
Save MirkoBonadei/9f43e7993f868cd793cb to your computer and use it in GitHub Desktop.
coffee machine tests with meck
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-module(coffee_fsm). | |
-include_lib("eunit/include/eunit.hrl"). | |
-behaviour(gen_fsm). | |
-export([start_link/0, init/1]). | |
-export([stop/0, terminate/3]). | |
-export([americano/0, cappuccino/0, tea/0, espresso/0, cup_removed/0, pay/1, cancel/0]). | |
-export([selection/2, payment/2, remove/2, handle_event/3, handle_info/3, code_change/4, handle_sync_event/4]). | |
%% State names macro | |
-define(SELECTION, "selection"). | |
-define(PAYMENT, "payment"). | |
-define(REMOVE, "remove"). | |
%% HW messages macro | |
-define(MAKE_YOUR_SELECTION, "Make Your Selection"). | |
-define(PLEASE_PAY, "Please pay:~w"). | |
-define(PREPARING_DRINK, "Preparing Drink."). | |
-define(REMOVE_DRINK, "Remove Drink."). | |
%% Client functions | |
start_link() -> | |
gen_fsm:start_link({local, ?MODULE}, ?MODULE, [], []). | |
stop() -> | |
gen_fsm:send_all_state_event(?MODULE, stop). | |
americano() -> | |
gen_fsm:send_even(?MODULE, {selection, americano, 150}). | |
cappuccino() -> | |
gen_fsm:send_even(?MODULE, {selection, cappuccino, 150}). | |
espresso() -> | |
gen_fsm:send_even(?MODULE, {selection, espresso, 100}). | |
tea() -> | |
gen_fsm:send_even(?MODULE, {selection, tea, 100}). | |
cup_removed() -> | |
gen_fsm:send_event(?MODULE, cup_removed). | |
pay(Coin) -> | |
gen_fsm:send_event(?MODULE, {pay, Coin}). | |
cancel() -> | |
gen_fsm:send_event(?MODULE, cancel). | |
%% FSM callbacks | |
init([]) -> | |
hw:reboot(), | |
hw:display(?MAKE_YOUR_SELECTION, []), | |
process_flag(trap_exit, true), %% TODO: make some test on the termination/cleanup phase | |
{ok, selection, []}. | |
selection({selection, Type, Price}, _LoopData) -> | |
hw:display(?PLEASE_PAY, [Price]), | |
{next_state, ?PAYMENT, {Type, Price, 0}}; | |
selection({pay, Coin}, LoopData) -> | |
hw:return_change(Coin), | |
{next_state, ?SELECTION, LoopData}; | |
selection(_OtherMessage, LoopData) -> | |
{next_state, ?SELECTION, LoopData}. | |
payment({pay, Coin}, {Type, Price, Paid}) when Paid + Coin < Price -> | |
NewPaid = Paid + Coin, | |
ToPay = Price - NewPaid, | |
hw:display(?PLEASE_PAY, [ToPay]), | |
{next_state, ?PAYMENT, {Type, Price, Paid + Coin}}; | |
payment({pay, Coin}, {_Type, Price, Paid}) when Paid + Coin >= Price -> | |
hw:display(?PREPARING_DRINK, []), | |
hw:return_change((Paid + Coin) - Price), | |
hw:drop_cup(), | |
hw:display(?REMOVE_DRINK, []), | |
{next_state, ?REMOVE, null}; | |
payment(cancel, {_Type, _Price, _Paid}) -> | |
hw:display(?MAKE_YOUR_SELECTION, []), | |
{next_state, ?SELECTION, null}; | |
payment(_OtherMessage, {Type, Price, Paid}) -> | |
{next_state, ?PAYMENT, {Type, Price, Paid}}. | |
remove(cup_removed, _LoopData) -> | |
hw:display(?MAKE_YOUR_SELECTION, []), | |
{next_state, ?SELECTION, null}; | |
remove({pay, Coin}, _LoopData) -> | |
hw:return_change(Coin), | |
{next_state, ?REMOVE, null}; | |
remove(_OtherMessage, _LoopData) -> | |
{next_state, ?REMOVE, null}. | |
handle_event(stop, _State, LoopData) -> | |
{stop, normal, LoopData}. | |
terminate(_Reason, ?PAYMENT, {_Type, _Price, _Paid}) -> | |
%% TODO: test with meck that we invoke HW for the remainder if | |
%% we are in PAYMENT state | |
ok; | |
terminate(_Reason, _State, _LoopData) -> | |
ok. | |
handle_info(_Info, _State, _LoopData) -> | |
ok. | |
code_change(_OldVsn, _State, _LoopData, _Extra) -> | |
ok. | |
handle_sync_event(_Event, _From, _State, _LoopData) -> | |
ok. | |
%% Tests | |
-ifdef(TEST). | |
when_in_selection_state_and_user_makes_a_selection_next_state_is_payment_test() -> | |
Type = something_to_drink, | |
Price = 100, | |
meck:new(hw), | |
meck:expect( | |
hw, | |
display, | |
fun(Msg, Args) -> | |
?assertEqual(?PLEASE_PAY, Msg), | |
?assertEqual([100], Args) | |
end | |
), | |
?assertMatch( | |
{next_state, ?PAYMENT, {Type, Price, 0}}, | |
?MODULE:selection({selection, Type, Price}, []) | |
), | |
?assert(meck:validate(hw)), | |
?assertEqual(1, meck:num_calls(hw, display, 2)), | |
meck:unload(hw). | |
when_in_selection_state_and_user_insert_coin_we_remain_in_selection_state_test() -> | |
LoopData = [], | |
meck:new(hw), | |
meck:expect( | |
hw, | |
return_change, | |
fun(Change) -> | |
?assertEqual(100, Change) | |
end | |
), | |
?assertMatch( | |
{next_state, ?SELECTION, LoopData}, | |
?MODULE:selection({pay, 100}, LoopData) | |
), | |
?assert(meck:validate(hw)), | |
?assertEqual(1, meck:num_calls(hw, return_change, 1)), | |
meck:unload(hw). | |
when_in_selection_state_other_events_are_ignored_test() -> | |
LoopData = [], | |
meck:new(hw, [passthrough]), | |
?assertMatch( | |
{next_state, ?SELECTION, LoopData}, | |
?MODULE:selection(cancel, LoopData) | |
), | |
?assertMatch( | |
{next_state, ?SELECTION, LoopData}, | |
?MODULE:selection(cup_removed, LoopData) | |
), | |
?assertEqual(0, meck:num_calls(hw, display, '_')), | |
?assertEqual(0, meck:num_calls(hw, return_change, '_')), | |
?assertEqual(0, meck:num_calls(hw, drop_cup, '_')), | |
?assertEqual(0, meck:num_calls(hw, reboot, '_')), | |
?assertEqual(0, meck:num_calls(hw, prepare, '_')), | |
meck:unload(hw). | |
when_in_payment_state_user_inserts_not_enough_coins_we_are_still_in_payment_state_test() -> | |
Type = something, | |
Price = 100, | |
Payed = 0, | |
Inserted = 10, | |
ExpectedToBeInserted = Payed + Inserted, | |
meck:new(hw), | |
meck:expect( | |
hw, | |
display, | |
fun(Msg, Args) -> | |
?assertEqual(?PLEASE_PAY, Msg), | |
?assertEqual([90], Args) | |
end | |
), | |
?assertMatch( | |
{next_state, ?PAYMENT, {Type, Price, ExpectedToBeInserted}}, | |
?MODULE:payment({pay, Inserted}, {Type, Price, Payed}) | |
), | |
?assert(meck:validate(hw)), | |
?assertEqual(1, meck:num_calls(hw, display, 2)), | |
meck:unload(hw). | |
when_in_payment_state_user_inserts_enough_coins_we_go_into_the_remove_state_test() -> | |
Type = something, | |
Price = 100, | |
Payed = 0, | |
Inserted = 200, | |
meck:new(hw), | |
meck:expect( | |
hw, | |
display, | |
fun(Msg, _Args) -> | |
%% not a good assertion. But I can't check order of calls when | |
%% defining an expectation. We can check better in the history | |
%% of the mock after the calls have been made | |
?assert((Msg =:= ?PREPARING_DRINK) or (Msg =:= ?REMOVE_DRINK)) | |
end | |
), | |
meck:expect( | |
hw, | |
return_change, | |
fun(Coin) -> | |
?assertEqual(100, Coin) | |
end | |
), | |
meck:expect(hw, drop_cup, 0, ok), | |
?assertMatch( | |
{next_state, ?REMOVE, null}, | |
?MODULE:payment({pay, Inserted}, {Type, Price, Payed}) | |
), | |
%% History = meck:history(hw), | |
%% lists:foreach(fun(Elem) -> io:format("~p~n", [Elem]) end, History), | |
%%[{<0.1931.0>,{hw,display,["Preparing Drink.",[]]},ok}, | |
%% {<0.1931.0>,{hw,return_change,"d"},ok}, | |
%% {<0.1931.0>,{hw,drop_cup,[]},ok}, | |
%% {<0.1931.0>,{hw,display,[[...]|...]},ok}]]) | |
%% History seems a good place for finding informations about the | |
%% order of the calls. At the moment this aspect is not tested | |
?assert(meck:validate(hw)), | |
?assertEqual(2, meck:num_calls(hw, display, 2)), | |
?assertEqual(1, meck:num_calls(hw, return_change, 1)), | |
?assertEqual(1, meck:num_calls(hw, drop_cup, 0)), | |
%% let's try capture/5 (this is better bacause we can understand the sequence of the calls) | |
?assertEqual(?PREPARING_DRINK, meck:capture(first, hw, display, '_', 1)), | |
?assertEqual(?REMOVE_DRINK, meck:capture(last, hw, display, '_', 1)), | |
%% but we have to check by calling Args and this is not the easiest way. | |
%% for a perfect validation of the sequence it would be great to use history | |
%% beacuse we have to display "remove drink" only after the cup has been dropped | |
meck:unload(hw). | |
when_in_payment_state_user_push_cancel_machine_returns_to_selection_state_test() -> | |
meck:new(hw), | |
meck:expect( | |
hw, | |
display, | |
fun(Msg, Args) -> | |
?assertEqual(?MAKE_YOUR_SELECTION, Msg), | |
?assertEqual([], Args) | |
end | |
), | |
?assertMatch( | |
{next_state, ?SELECTION, null}, | |
?MODULE:payment(cancel, {something_to_drink, 100, 0}) | |
), | |
?assertEqual(1, meck:num_calls(hw, display, 2)), | |
meck:unload(hw). | |
when_in_payment_state_other_events_are_ignored_test() -> | |
Type = something_to_drink, | |
Price = 100, | |
Inserted = 10, | |
meck:new(hw, [passthrough]), | |
?assertMatch( | |
{next_state, ?PAYMENT, {Type, Price, Inserted}}, | |
?MODULE:payment(cup_removed, {Type, Price, Inserted}) | |
), | |
?assertMatch( | |
{next_state, ?PAYMENT, {Type, Price, Inserted}}, | |
?MODULE:payment({selection, {something_other, 300, 0}}, {Type, Price, Inserted}) | |
), | |
?assertEqual(0, meck:num_calls(hw, display, '_')), | |
?assertEqual(0, meck:num_calls(hw, return_change, '_')), | |
?assertEqual(0, meck:num_calls(hw, drop_cup, '_')), | |
?assertEqual(0, meck:num_calls(hw, reboot, '_')), | |
?assertEqual(0, meck:num_calls(hw, prepare, '_')), | |
meck:unload(hw). | |
when_in_remove_state_after_cup_removed_event_machine_returns_in_selection_test() -> | |
meck:new(hw), | |
meck:expect(hw, display, fun(Msg, _Args) -> ?assertEqual(?MAKE_YOUR_SELECTION, Msg) end), | |
?assertMatch( | |
{next_state, ?SELECTION, null}, | |
?MODULE:remove(cup_removed, null) | |
), | |
?assertEqual(1, meck:num_calls(hw, display, 2)), | |
meck:unload(hw). | |
when_in_remove_state_user_inserts_coin_machine_is_still_in_remove_state_test() -> | |
meck:new(hw), | |
meck:expect(hw, return_change, fun(Coin) -> ?assertEqual(10, Coin) end), | |
?assertMatch( | |
{next_state, ?REMOVE, null}, | |
?MODULE:remove({pay, 10}, null) | |
), | |
?assertEqual(1, meck:num_calls(hw, return_change, 1)), | |
meck:unload(hw). | |
when_in_remove_state_other_events_are_ignored_test() -> | |
meck:new(hw, [passthrough]), | |
?assertMatch( | |
{next_state, ?REMOVE, null}, | |
?MODULE:remove({selection, {something_to_drink, 100, 0}}, null) | |
), | |
?assertMatch( | |
{next_state, ?REMOVE, null}, | |
?MODULE:remove(cancel, null) | |
), | |
?assertEqual(0, meck:num_calls(hw, display, '_')), | |
?assertEqual(0, meck:num_calls(hw, return_change, '_')), | |
?assertEqual(0, meck:num_calls(hw, drop_cup, '_')), | |
?assertEqual(0, meck:num_calls(hw, reboot, '_')), | |
?assertEqual(0, meck:num_calls(hw, prepare, '_')), | |
meck:unload(hw). | |
%%when_fsm_is_going_down_change_is_returned_test() -> | |
%% meck:new(hw, [passthrough]), | |
%% meck:expect(hw, return_change, fun(Coin) -> ?assertEqual(50, Coin) end), | |
%% ?MODULE:start_link(), | |
%% ?MODULE:americano(), | |
%% ?MODULE:pay(50), | |
%% ?MODULE:stop(), | |
%% ?assertEqual(1, meck:num_calls(hw, return_change, 1)), | |
%% meck:unload(hw). | |
-endif. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-module(hw). | |
-compile(export_all). | |
display(Str, Arg) -> | |
io:format("Display: " ++ Str ++ "~n", Arg). | |
return_change(Payment) -> | |
io:format("Machine: returned ~w in change~n", [Payment]). | |
drop_cup() -> | |
io:format("Machine: dropped cup~n", []). | |
prepare(Type) -> | |
io:format("Machine: preparing ~p~n", [Type]). | |
reboot() -> | |
io:format("Machine: rebooted hardware~n", []). |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment