Unit testing 如何在erlang中进行依赖项注入和模拟?

Unit testing 如何在erlang中进行依赖项注入和模拟?,unit-testing,dependency-injection,erlang,mocking,Unit Testing,Dependency Injection,Erlang,Mocking,在用Java编写代码时,通过模仿协作对象来接受并使纯单元测试变得可能和容易是非常有帮助的 我发现在Erlang中执行同样的操作不那么简单,而且会产生更脏的代码 这很可能是我的错,因为我对Erlang非常陌生,并且对JUnit、EasyMock和java接口非常上瘾 假设我有一个愚蠢的函数: %% module mymod handle_announce(Announce) -> AnnounceDetails = details_db:fetch_details(Announce)

在用Java编写代码时,通过模仿协作对象来接受并使纯单元测试变得可能和容易是非常有帮助的

我发现在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\u db
stats\u db
是使用正确的参数调用的,并且返回值使用正确。
details\u db
stats\u db
生成正确值的能力在其他地方进行了测试

为了解决这个问题,我可以通过以下方式重构代码:

%% module mymod
handle_announce(Announce, [DetailsDb, StatsDb]) ->
    AnnounceDetails = DetailsDb:fetch_details(Announce),
    AnnounceStats = StatsDb:fetch_stats(Announce),
    {AnnounceDetails, AnnounceStats}.
并以这种方式进行测试(基本上是将调用直接存根到测试模块中):

它可以工作,但是应用程序代码变脏了,我总是要随身携带那些丑陋的模块列表

我尝试了几个模拟库(和()),但并不满意

如何对erlang代码进行单元测试


谢谢!

这里有两件事要考虑……/P> 您需要将所有代码分成两种不同类型的模块

  • 纯功能模块(又名无副作用模块)
  • 有副作用的模块
(您应该仔细阅读这些内容,并确保您理解其中的区别——最典型的副作用——以及示例代码中的副作用——是写入数据库)

纯功能的模块变得很容易测试。每个导出的函数(根据定义)在输入相同的值时总是返回相同的值。你可以使用Richard Carlsson和Mickeel Remond编写的框架。Bish bash bosh,job是一个很好的“un…”

关键的一点是,大约90%的代码应该在纯功能模块中—您可以大大减少您的问题(您可能认为这不是“解决”您的问题,而只是“减少”它—您基本上是对的…)

一旦实现了这种分离,对具有副作用的模块进行单元测试的最佳方法就是使用

我们这样做的方式不是使用模拟对象,而是在init_per_套件或init_per_测试函数中加载数据库,然后运行模块本身


最好的方法是尽快直接进行系统测试,尽管单元测试很难维护——因此有足够的单元测试让您进行系统测试往返,而不是更多(最好尽快删除db单元测试)Gordon是正确的,因为主要目标是测试无副作用的小功能

但是…好吧,测试集成也是可能的,所以让我们展示一下如何做到这一点

注射 避免使用带有参数化依赖项的列表。使用记录、流程字典、参数化模块。代码将不那么难看

接缝
不要将变量模块作为依赖项接缝,让流程成为接缝。硬编码注册的流程名称是注入依赖项的一个机会。

我同意Guthrie的说法。你会惊讶于你的逻辑有多少可以被拉进纯函数中


我最近与新的参数化模块联系在一起的一件事是使用参数化模块进行依赖项注入。这避免了参数列表和进程字典的问题。如果您可以使用erlang的最新版本,这可能也是一个很好的选择。

我只是直接回答了被问到的问题而不是试图判断作者是否应该这样做

使用,您可以为您的示例编写单元测试,如下所示:

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开发人员,最近爱上了Erlang,现在为上述项目做出了贡献。

感谢Gordon,解释得很好。我仍在尝试切换到函数范式。无论如何,在这个项目中,我正在编写(torrent tracker),所有调用都起源于web层并最终进入数据库,因此大多数模块都有副作用或依赖副作用。我将尝试使用标准测试框架。好的Erlang应该有很多小功能。重构并将代码分解为实用程序模块,你会惊讶地发现其中很少涉及到写入数据库。15-25lines在Erlang中是一个很长的函数。纯函数是一个接受一组参数,对它们进行计算并只返回一个值的函数-你应该有很多这样的参数。我实际上不同意你应该尽快进行集成测试。对于erlymock提供的参数化模块和模拟,给定的函数将是琐事l进行测试。您可以通过在测试期间模拟两个db模块在模块内部对其进行测试。您可以通过将db模块作为模块的参数在其他测试中使用此模块。任何在erlang中查看模拟的人都应该再看一看erlymock——它的主要问题是缺乏文档,因此您可以真正了解它我必须阅读源代码才能接受它。对有副作用的模块进行单元测试的问题是它们的维护成本。让我举一个具体的例子。在我们构建系统时,我们必须进行大量的性能调整和重构工作。每次我们这样做时,我们都必须重新调整数据库模式和其他内容。有系统测试ts意味着我们可以直接启动并进行测试,测试套件会告诉我们是否失败。如果我们有很多单元测试,它们将与db表示绑定,并且必须重写。我不建议使用流程字典
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().