使用Java编写代码时,拥抱composition和dependency injection非常有用,可以通过模拟协作对象来实现纯单元测试。
我发现在Erlang中做同样的事情并不那么简单,而且代码更脏。
这可能是我的错,因为我对Erlang很新,并且对JUnit,EasyMock和java接口非常沉迷......
假设我有这个愚蠢的功能:
%% module mymod
handle_announce(Announce) ->
AnnounceDetails = details_db:fetch_details(Announce),
AnnounceStats = stats_db:fetch_stats(Announce),
{AnnounceDetails, AnnounceStats}.
单元测试mymod
时,我只想证明使用正确的参数调用details_db
和stats_db
,并且正确使用了返回值。
details_db
和stats_db
生成正确值的能力在其他地方进行了测试。
要解决这个问题,我可以用这种方式重构我的代码:
%% module mymod
handle_announce(Announce, [DetailsDb, StatsDb]) ->
AnnounceDetails = DetailsDb:fetch_details(Announce),
AnnounceStats = StatsDb:fetch_stats(Announce),
{AnnounceDetails, AnnounceStats}.
以这种方式测试(基本上将调用直接存入测试模块):
%% module mymod_test
handle_announce_test() ->
R = mymod:handle_announce({announce, a_value}, [?MODULE, ?MODULE, ?MODULE]),
?assertEqual({details,stats}, R).
fetch_details({announce, a_value}) ->
details.
fetch_stats({announce, a_value}) ->
stats.
它可以工作,但应用程序代码变脏了,我总是要随身携带那些丑陋的模块列表。
我尝试了几个模拟库(erlymock和(this other one),但我并不满意。
如何对你的erlang代码进行单元测试?
谢谢!
答案 0 :(得分:22)
这里有两件事要考虑......
您需要将所有代码分成两种不同类型的模块:
(您应该阅读并确保您了解其中的差异 - 最典型的副作用 - 以及示例代码中的副作用 - 正在写入数据库)。
纯功能模块变得微不足道。每个导出的函数(按定义)总是在放入相同的值时返回相同的值。您可以使用Richard Carlsson和Mickael Remond编写的EUnit/Assert框架。 Bish-bash-bosh,工作很好'不......
关键是大约90%的代码应该是纯函数模块 - 这会大大缩小你的问题。 (你可能会认为这不是'解决'你的问题,只是'减少'它 - 你会大致正确......)
一旦实现了这种分离,对具有副作用的模块进行单元测试的最佳方法是使用standard test framework。
我们这样做的方法不是使用模拟对象 - 而是在init_per_suite或init_per_test函数中加载数据库,然后自己运行模块......
最好的方法是尽快直接进行系统测试,因为单元测试很难维护 - 所以足够的单元测试可以让你进行系统测试往返而不再需要(甚至可以尽快删除数据库单元测试。
答案 1 :(得分:4)
戈登是正确的,主要目标是测试小副作用自由功能。
但是......好吧,也可以测试集成,所以让我们展示一下如何做到这一点。
避免使用列表来携带参数化依赖项。使用记录,流程字典,参数化模块。代码将不那么难看。
不要将变量模块作为依赖关系,让流程成为接缝。对注册的进程名称进行硬编码是注入依赖项的机会。
答案 2 :(得分:4)
我是格思里所说的第二个。您可以将多少逻辑划分为纯函数,这会让您感到惊讶。
我最近使用新参数化模块进行的一项工作是使用参数化模块进行依赖注入。它避免了参数列表和流程词典的问题。如果你可以使用最新版本的erlang也可能是合适的。
答案 3 :(得分:3)
我只是回答直接问的问题,而不是试图判断作者是否应该这样做。
使用meck您可以为您编写单元测试示例,如下所示:
handle_announce_test() ->
%% Given
meck:new([details_db, stats_db]),
meck:expect(details_db, fetch_details, ["Announce"], "AnnounceDetails"),
meck:expect(stats_db, fetch_stats, ["Announce"], "AnnounceStats"),
%% When
Result = handle_announce("Announce"),
%% Then
?assertMatch({"AnnounceDetails", "AnnounceStats"}, Result),
%% Cleanup
meck:unload().
我使用字符串只是为了强调它们不是真正传递的东西而是伪造的值。由于语法高亮,它们很容易在测试代码中找到。
说实话,我是一名前Java开发人员,深深地爱着Mockito最近转向Erlang,现在为上述项目做出了贡献。