【问题标题】:How do I do dependency injection and mocks in erlang?如何在 erlang 中进行依赖注入和模拟?
【发布时间】:2009-07-29 19:36:12
【问题描述】:

在用 Java 编写代码时,采用 compositiondependency 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_dbstats_db使用正确的参数调用,并且返回值使用正确。 details_dbstats_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 代码进行单元测试?

谢谢!

【问题讨论】:

    标签: unit-testing dependency-injection erlang mocking


    【解决方案1】:

    这里有两件事要考虑...

    您需要将所有代码分成两种不同类型的模块

    • 纯功能模块(又称无副作用模块)
    • 有副作用的模块

    (您应该阅读并确保您了解差异 - 最典型的副作用 - 以及示例代码中的副作用 - 正在写入数据库)。

    纯函数式的模块测试变得微不足道。当输入相同的值时,每个导出的函数(根据定义)总是返回相同的值。您可以使用 Richard Carlsson 和 Mickael Remond 编写的EUnit/Assert 框架。 Bish-bash-bosh,干得不错……

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

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

    我们这样做的方式不是使用模拟对象 - 而是在 init_per_suite 或 init_per_test 函数中加载数据库,然后自己运行模块...

    最好的方法是尽快直接进行系统测试,因为单元测试很难维护 - 所以足够的单元测试可以让您进行系统测试往返,仅此而已(最好尽快删除数据库单元测试)。

    【讨论】:

    • 谢谢戈登,解释得很好。我仍在尝试切换到功能范式。无论如何,在我正在编写的这个项目(一个种子跟踪器)中,所有调用都来自 Web 层并最终进入数据库,因此大多数模块确实具有或依赖于副作用。我会试试标准的测试框架。
    • 好的Erlang是有很多小功能。将您的代码重构并分解为实用模块,您会惊讶地发现其中很少涉及写入数据库。 15 - 25 行在 Erlang 中是一个很长的函数。纯函数是一个接受一组参数,对它们进行计算并返回一个值的函数——你应该有很多。
    • 我实际上不同意您应该尽快转向集成测试。使用 erlymock 提供的参数化模块和模拟,给定的函数将很容易测试。您可以通过在测试期间模拟两个 db 模块来在模块本身内部对其进行测试。通过将 db 模块作为模块的参数,您可以在其他测试中使用此模块。任何在 erlang 中查看 mocking 的人都应该再看看 erlymock——它的主要问题是缺乏文档,所以你真的必须阅读源代码才能接受它。
    • 对具有副作用的模块进行单元测试的问题是它们的维护成本。让我举一个具体的例子。在构建系统时,我们必须做很多性能调整和重组工作。每次我们这样做时,我们都必须重新调整数据库模式和东西。进行系统测试意味着我们可以直接启动并执行它,如果我们失败了,测试套件会告诉我们。如果我们有很多单元测试 - 它们将与 db 表示相关联并且必须重写。
    【解决方案2】:

    我赞同格思里所说的话。你会惊讶于你的逻辑有多少可以被提取到纯函数中。

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

    【讨论】:

      【解决方案3】:

      Gordon 是正确的,主要目标是测试无副作用的小功能。

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

      注入

      避免列表携带参数化的依赖关系。使用记录、过程字典、参数化模块。代码不会那么难看。

      接缝

      不要将变量模块视为依赖接缝,让流程成为接缝。硬编码已注册的进程名称会失去注入依赖项的机会。

      【讨论】:

      • 我不推荐使用进程字典。这一切看起来都很好,很容易,但它是共享状态,最终它会很难找到错误。在 Erlang 中,很难对流程的生命周期进行概念化和合理化(尽管您可能认为可以),而流程生命周期的变化是您的干净流程字典出现严重错误的地方。我从痛苦的经历中知道这一点:(
      • @Christian,指向 emock 的链接似乎已失效。是不是一样的:github.com/noss/emock?
      【解决方案4】:

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

      使用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,现在为上述项目做出贡献。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-03-23
        • 2017-03-08
        • 1970-01-01
        • 1970-01-01
        • 2011-08-01
        • 1970-01-01
        • 1970-01-01
        • 2020-06-06
        相关资源
        最近更新 更多