【问题标题】:Elixir/ExUnit: how to test functions with system calls most elegantly?Elixir/ExUnit:如何用系统调用最优雅地测试函数?
【发布时间】:2017-03-20 09:22:27
【问题描述】:

情况

通常,像 ExUnit 这样的单元测试应该是自包含的,包含输入、函数调用和所需的输出,以便测试可以在任何系统上运行,并且无论环境如何都能正确测试。

另一方面,如果您的应用程序执行系统调用,例如使用 Elixir 的 System.cmd/3 或 Erlang 的 :os.cmd/1 并使用结果,您的测试可能会因为不同/更新的二进制文件、环境变化等原因而得到不同的结果,不同的操作系统等等。

当然,在这些情况下测试失败是件好事,这样您对现实生活情况的覆盖范围就会增加。然而,在开发时,您会希望首先让您的函数做正确的事情,然后才能做正确的事情。如果外部世界发生变化,要始终以可预测的方式运行测试是很困难的,甚至是不可能的。

此外,您可能想要测试很少或几乎不会发生的情况,但您的系统调用不会为您提供该信息,因为它确实很少发生。您需要以某种方式模拟系统调用的输出并将其与程序的内部逻辑分开。


示例

为简单起见(同样的原则适用于更复杂的情况),请考虑读取系统的启动时间并根据清理后的结果做出响应:

def what_time do
  time =
    :os.cmd('who -b | cut -d\' \' -f14') # Returns something like '13:50\n'
    |> to_string
    |> String.trim("\n")
    |> String.split(":")
    |> List.to_tuple
  case time do
    {"12", "00"} -> {:ok, "It's High Noon!"}
    _ -> {:error, "meh"}
  end
end

只有在特定时间重新启动系统才能正确测试此功能,这当然是不合理的。但是由于输出的格式大致已知,您可以创建一个测试值列表,例如['16:04', '23:59', '12:00', "12:00", 2, "xyz", '1.0"],并在没有系统调用的情况下测试解析部分,然后像往常一样将其与您的预期结果进行比较。

天真的方法

但是这是怎么做到的呢?系统调用是函数中的第一件事,因此如果将其取出到单独的函数中,则可以测试系统调用,但这对您没有多大帮助,因为系统调用本身就是问题:

def what_time do
  time = get_time
    |> to_string
    [...]
end

def get_time do
  :os.cmd('who -b | cut -d\' \' -f14') # Returns something like '13:50\n'
end

稍微好一点...

如果您添加另一个仅解析字符串/字符列表的辅助方法,您可以实现您想要的,同时将系统调用本身设为私有:

def what_time do
  what_time_helper(get_time())
end

def what_time_helper(time) do
  time =
    time
    |> to_string
    [...]
  end
end

defp get_time do
  :os.cmd('who -b | cut -d\' \' -f14') # Returns something like '13:50\n'
end

现在可以调用ExUnit案例中的辅助测试函数,普通程序也可以调用普通函数了。

...但不好?

虽然最后一个想法在实践中有效,但我觉得它不是很优雅。我可以看到以下缺点:

  1. 每个函数都需要拆分为私有系统调用、公共助手和公共普通方法,函数数量增加了三倍。由于不必要的分区,生成的代码更长且更难阅读。
  2. 辅助方法需要公开才能进行测试,但不应公开。因此,必须编写额外的文档,API 参考变得更长,并且该方法必须进行更多检查以确保安全操作(而以前,只有系统调用本身产生的值才会发生)。
  3. 虽然小main函数只调用了一个预定义集合的另一个,但它不能包含在测试覆盖范围内。这个抱怨有点吹毛求疵,但我想如果使用自动测试工具以代码行或函数数量显示测试覆盖率,就会有问题。

问题

所以,我的问题是:

  • 如何在测试中正确处理此类情况,例如。 G。与 ExUnit?
  • 如何将系统调用与内部逻辑分离并减少样板函数的数量?
  • 是否有任何工具或通用方法通常在函数式编程中如何完成?

【问题讨论】:

  • 你可以用github.com/jjh42/mock模拟:os.cmd/1
  • 不确定这是否适用于 Elixir,但至少在一个 C++ 应用程序中,我们通过 wrapper 进行所有系统调用,以便我们可以轻松地将它们 mock 出来。
  • @merlin2011 是的,这就是我在面向对象语言中所做的方式,因为您可以轻松地提供符合相同接口的真实和测试实现。没有任何对象,我目前正在努力寻找正确的方法。
  • 请记住,模拟 :os 模块功能可能很困难,至少对于 :meck: github.com/eproxus/meck#caveats 来说是困难的。将它们包装在实用程序模块中并模拟它可能是个好主意。

标签: unit-testing functional-programming elixir system-calls ex-unit


【解决方案1】:

关于样式以及如何将功能拆分为单独的功能,或者将其保留在一个功能中,这取决于您的胃口,以及您希望如何处理未来的代码。每种解决方案(多合一功能或分离)各有利弊。

关于测试方面,最好的办法是将操作系统调用视为外部 API 调用。这样做,您可以轻松地在测试中使用模拟和存根,这样您就可以控制测试的内容和方式。

Jose Valim 有一个very comprehensive blog post 关于模拟以及您应该如何测试外部调用。我建议先通读一遍。

如果你用谷歌搜索,很少有库可以为你存根/模拟东西:

  • ex_stub
  • meck - 在 Erlang 中,但我相信也可以在 Elixir 中使用
  • mock

【讨论】:

  • 感谢您的博文和建议,我会阅读并尝试制定适合我示例的代码。
  • 请注意,你应该只使用 Elixir 模拟作为最后的后备,并且如果你知道它们究竟做了什么(主要是全局模块名称)。有更简洁但稍微麻烦的方法来执行此操作。
  • @cdegroot 你的意思是像 José 这样的模拟在链接的帖子中展示了它们(基本上是在测试时替换普通类的类)?如果是,你会建议什么?
  • 我在多个项目中使用Elixir中的:meck,使用起来非常简单有效。
  • @nietaki 感谢您的确认,我没有在 Elixir 本身中使用它。它在 Erlang 世界的 EUnit / CT 中完美运行。
猜你喜欢
  • 2013-11-04
  • 2019-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-20
  • 2020-03-20
  • 2016-06-28
  • 2021-09-28
相关资源
最近更新 更多