Skip to content

Instantly share code, notes, and snippets.

@MirkoBonadei
Last active August 29, 2015 14:07
Show Gist options
  • Save MirkoBonadei/9f43e7993f868cd793cb to your computer and use it in GitHub Desktop.
Save MirkoBonadei/9f43e7993f868cd793cb to your computer and use it in GitHub Desktop.
coffee machine tests with meck
-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.
-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