7

gen_fsmでFSMを実装していると考えてください。いくつかのStateNameのいくつかのイベントの場合、データベースにデータを書き込み、結果をユーザーに返信する必要があります。したがって、次のStateNameは関数で表されます。

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.

ここで、my_db_module:writeは、実際のデータベース書き込みを実装する非機能コードの一部です。

このコードには2つの大きな問題があります。1つは、FSMの純粋関数型の概念が非機能型コードの一部と混在しているため、FSMの単体テストも不可能です。次に、FSMを実装するモジュールは、my_db_moduleの特定の実装に依存しています。

私の意見では、2つの解決策が可能です。

  1. my_db_module:write_asyncを、非同期メッセージをプロセス処理データベースに送信するように実装し、statenameで応答せず、FromをStateDataに保存し、wait_for_db_answerに切り替えて、db管理プロセスからの結果を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.
    

    このような実装の利点は、実際のデータベースに触れることなく、eunitモジュールから任意のメッセージを送信できることです。ソリューションは、dbが以前に応答した場合、FSMが状態を変更するか、別のプロセスがsave_dataをFSMに送信する可能性のある競合状態に悩まされます。

  2. StateDataのinit/1中に記述されたコールバック関数を使用します。

    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.
    

    このソリューションは競合状態に悩まされることはありませんが、FSMが多くのコールバックを使用する場合、コードを実際に圧倒します。実際の関数コールバックに変更すると単体テストが可能になりますが、関数型コードの分離の問題は解決されません。

私はこのすべての解決策に満足しているわけではありません。この問題を純粋なOTP/Erlangの方法で処理するためのレシピはありますか?それは、OTPとeunitの原則を過小評価するという私の問題かもしれません。

4

1 に答える 1

2

これを解決する 1 つの方法は、データベース モジュールの依存性注入を使用することです。

状態レコードを次のように定義します。

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

これで、gen_server の init/1 に db_mod を注入できます。

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

コードを取得したら、次のようにします。

 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.

別のモジュールでテストするときに、データベース モジュールをオーバーライドする機能があります。スタブの注入が非常に簡単になり、データベース コードの表現を必要に応じて変更できるようになりました。

もう 1 つの方法は、meckテスト時にデータベース モジュールをモックするようなツールを使用することですが、通常は構成可能にすることを好みます。

ただし、一般的には、複雑なコードを独自のモジュールに分割して、個別にテストできるようにする傾向があります。私は他のモジュールの単体テストをあまり行うことはめったになく、そのような部分のエラーを処理するために大規模な統合テストを好みます。Common Test、PropEr、Triq、および Erlang QuickCheck を見てください (後者はオープン ソースではなく、フル バージョンも無料ではありません)。

于 2012-10-08T12:14:35.487 に答える