情况
通常,像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案例中调用helper测试函数,普通程序可以调用普通函数。
但是这种方法似乎不太优雅。我可以看到以下缺点:
1.每个函数都需要分成私有系统调用、公共辅助和公共正常方法,使函数数量增加三倍。由于没有必要的分区,结果代码更长,更难以阅读。
2.为了进行测试,helper方法需要是公共的,但不应该对外暴露。因此,必须编写额外的文档,API参考变得更长,并且该方法必须执行更多的检查以确保安全操作(而之前,只有由系统调用本身产生的值才可能发生)。
3.虽然小的主函数只调用另一个预定义集合,但它不能包含在测试覆盖范围内。这个抱怨有点吹毛求疵,但我想象一下如果使用自动化测试工具显示代码行或函数数量的测试覆盖率,这会变得很麻烦。
问题:
那么我的问题是:
1.如何在测试中正确处理这种情况,例如使用ExUnit?
2.如何将系统调用与内部逻辑分离并减少样板函数的数量?
3.在函数式编程中通常如何处理这种情况?
:os.cmd/1
。 - Dogbert:os
模块函数可能会很困难,至少对于:meck
来说是这样的:https://github.com/eproxus/meck#caveats。将它们包装在实用程序模块中并进行模拟可能是一个好主意。 - nietaki