7
votes

Consider I have a FSM implemented with gen_fsm. For a some Event in a some StateName I should write data to database and reply to user the result. So the following StateName is represented by a function:

statename(Event, _From, StateData)  when Event=save_data->
    case my_db_module:write(StateData#state.data) of
         ok -> {stop, normal, ok, StateData};
         _  -> {reply, database_error, statename, StateData)
    end.

where my_db_module:write is a part of non-functional code implementing actual database write.

I see two major problems with this code: the first, a pure functional concept of FSM is mixed by part of non-functional code, this also makes unit testing of FSM impossible. Second, a module implementing a FSM have dependency on particular implementation of my_db_module.

In my opinion, two solutions are possible:

  1. Implement my_db_module:write_async as sending an asynchronous message to some process handling database, do not reply in statename, save From in StateData, switch to wait_for_db_answer and wait result from db management process as a message in a handle_info.

    statename(Event, From, StateData)  when Event=save_data->
        my_db_module:write_async(StateData#state.data),
        NewStateData=StateData#state{from=From},
        {next_state,wait_for_db_answer,NewStateData}
    
    handle_info({db, Result}, wait_for_db_answer, StateData) ->
        case Result of
             ok -> gen_fsm:reply(State#state.from, ok),
                   {stop, normal, ok, State};
             _  -> gen_fsm:reply(State#state.from, database_error),
                   {reply, database_error, statename, StateData)
        end.
    

    Advantages of such implementation is possibility to send arbitrary messages from eunit modules without touching actual database. The solution suffers from possible race conditions, if db reply earlier, that FSM changes state or another process send save_data to FSM.

  2. Use a callback function, written during init/1 in StateData:

    init([Callback]) ->
    {ok, statename, #state{callback=Callback}}.
    
    statename(Event, _From, StateData)  when Event=save_data->
        case StateData#state.callback(StateData#state.data) of
             ok -> {stop, normal, ok, StateData};
              _  -> {reply, database_error, statename, StateData)
    end.
    

    This solution doesn't suffer from race conditions, but if FSM uses many callbacks it really overwhelms the code. Although changing to actual function callback makes unit testing possible it doesn't solves the problem of functional code separation.

I am not comfortable with all of this solutions. Is there some recipe to handle this problem in a pure OTP/Erlang way? Of may be it is my problem of understating principles of OTP and eunit.

1

1 Answers

2
votes

One way to solve this is via Dependency Injection of the database module.

You define your state record as

 -record(state, { ..., db_mod }).

And now you can inject db_mod upon init/1 of the gen_server:

 init([]) ->
    {ok, DBMod} = application:get_env(my_app, db_mod),
    ...
    {ok, #state { ..., db_mod = DBMod }}.

So when we have your code:

 statename(save_data, _From,
           #state { db_mod = DBMod, data = Data } = StateData) ->
   case DBMod:write(Data) of
     ok -> {stop, normal, ok, StateData};
     _  -> {reply, database_error, statename, StateData)
   end.

we have the ability to override the database module when testing with another module. Injecting a stub is now pretty easy and you can thus change the database code representation as you see fit.

Another alternative is to use a tool like meck to mock the database module when you are testing, but I usually prefer making it configurable.

In general though, I tend to split the code which is complex into its own module so it can be tested separately. I rarely do much unit testing of other modules and prefer large-scale integration tests to handle errors in such parts. Take a look at Common Test, PropEr, Triq and Erlang QuickCheck (The latter is not open source, nor is the full version free).