Elixir / ExUnit:如何使用系统调用最优雅地测试函数?

时间:2017-03-20 09:22:28

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

情况

通常,像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

稍微好一些......

如果你添加另一个只解析字符串/ charlist的帮助方法,你可以实现你想要的,同时使系统调用本身是私有的:

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. 虽然小主函数只使用预定义的集合调用另一个函数,但它不能包含在测试覆盖率中。这个投诉有点挑剔,但我认为如果使用自动测试工具在代码行或函数数量中显示测试覆盖率,就会出现问题。
  4. 问题

    所以,我的问题是:

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

1 个答案:

答案 0 :(得分:3)

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

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

Jose Valim有一个关于模拟的very comprehensive blog post以及你应该如何测试外部电话。我建议先阅读。

如果你在谷歌周围,很少有库可以为你存根/嘲笑: