考虑我使用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是实现实际数据库写入的非功能代码的一部分。
我看到这个代码存在两个主要问题:第一,FSM的纯功能概念与非功能代码的一部分混合在一起,这也使得FSM的单元测试变得不可能。其次,实现FSM的模块依赖于my_db_module的特定实现。
在我看来,有两种解决方案是可能的:
实现my_db_module:write_async作为向某个进程处理数据库发送异步消息,不回复statename,在StateData中保存From,切换到wait_for_db_answer并将数据库管理进程中的结果作为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。
使用在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的原则。
答案 0 :(得分:2)
解决此问题的一种方法是通过数据库模块的依赖注入。
您将州记录定义为
-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.
我们可以在使用其他模块进行测试时覆盖数据库模块。现在可以很容易地注入存根,因此您可以根据需要更改数据库代码表示。
另一种方法是在测试时使用像meck
这样的工具来模拟数据库模块,但我通常更喜欢使其可配置。
总的来说,我倾向于将复杂的代码拆分为自己的模块,因此可以单独测试。我很少对其他模块进行大量单元测试,而更喜欢大规模集成测试来处理这些部件中的错误。看看Common Test,PropEr,Triq和Erlang QuickCheck(后者不是开源的,也不是免费的完整版)。