3

情况

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

另一方面,如果您的应用程序执行系统调用,例如使用 ElixirSystem.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 函数只调用了另外一个预定义的集合,但它不能包含在测试覆盖范围内。这个抱怨有点吹毛求疵,但我想如果使用自动测试工具来显示代码行或函数数量的测试覆盖率,就会有问题。

问题

所以,我的问题是:

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

1 回答 1

3

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

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

Jose Valim 有一篇非常全面的博客文章,介绍了模拟以及您应该如何测试外部调用。我建议先通读一遍。

如果您四处搜索,很少有库可以为您存根/模拟事物:

于 2017-03-20T09:34:11.413 回答