测试 Elixir 函数的单元测试是否正常

3
我对Elixir和函数式编程都比较陌生,我很难正确地对由其他函数组成的函数进行单元测试。通常的问题是:当我有一个使用其他函数g、h等的函数f时,应该采用哪种方法来测试整个函数呢?
从面向对象编程的世界中出发,首先想到的方法是注入与f有关的函数。我可以单元测试g、h等函数,并将它们作为参数注入f。然后,f的单元测试只需要确保按预期调用这些被注入的函数即可。但这种做法感觉过度拟合了,并且对于函数组合应该是一件便宜的事情以及你不应该在整个代码库中传递所有这些参数的函数式思维方式来说,是一种总体上繁琐的做法。
我也可以将g、h等函数以及f视为黑盒进行单元测试,这样感觉是正确的做法,但是f的复杂度会急剧增加。编写简单可扩展的测试是单元测试的主要目的之一。
为了更具体地说明这一观点,我会放置一个将其他函数组合在内的函数的例子,并且我不知道如何正确地进行单元测试。特别的,这段代码是用于RESTful风格下资源的创建的插件,注意一些“依赖项”是纯函数(例如validate_account_admin),但其他则不是(Providers.create)。
  def call(conn, _opts) do
    account_uuid = conn.assigns.current_user.account["uuid"]

    with {:ok, conn}      <- Http.Authorization.validate_account_admin(conn),
         {:ok, form_data} <- Http.coerce_form_data(conn, FormData),
         {:ok, provider}  <- Providers.create(FormData.to_provider(form_data), account_uuid: account_uuid) do
      Http.respond_create(conn, Http.provider_path(provider))
    else
      {:error, reason, messages} -> Http.handle_error(conn, reason, messages)
    end
  end

谢谢!

1个回答

3
也许这个回答会比较主观,因为对于这种问题可能没有完美且终极的答案。
你的假设在使用公共函数嵌套其他公共函数方面是错误的。在业务逻辑区域中,你根本不应该这样做,因为它们应该是分离的,唯一可以这样做的地方 - 实际上 - 是在控制器中,但你使用集成测试来测试控制器,而不是单元测试,在这种测试中,你关心的只是正确和有效的响应。
我喜欢 Erlang 显式声明哪些函数应该是公共的方法,使用 `export` 子句来实现。在 Elixir 中,你也应该遵循这个方法,无论模块中应该隐藏什么,都应该使用 `defp` 和 `defmacrop` 分别声明私有函数和私有宏。
你的单元测试应该遵循黑盒子规则 - 你关心基于输入的输出。就这样。测试是愚蠢的,并不知道被测试的函数长什么样子,包含了什么内容。
在你的例子中,你在 Plug 的 `call` 函数中使用了一些函数,我非常确定这个插件做的事情超过了它应该做的 - 记住“单一职责原则”。“with”语句是多余的 - 插件检查前一个插件的输出结果以继续进行 - 这是`case`内部的`case`,就像 `with` 一样。
考虑到你有新的插件,你可以在插件中使用除了 `call` 和 `init` 之外的一些额外函数来定义实际工作的私有函数。这个行为可能会帮助你组织代码,并避免创建链式模块,从使用和责任的角度来看。
然后,单元测试将变得更容易,因为你将测试隔离的插件。
假设你按以下方式调用了这个插件:
plug MyPlug

you would rewrite into:

plug :validate_is_admin
plug :coerce_form_data
plug :create_from_form_data

也许有些简化了,但我希望你能理解我的意思。
TL;DR:将函数拆分为较小的部分,并在隔离中进行测试。将内部计算隐藏在私有函数中,仅测试公共API。

你建议将程序 P 建模为一个函数链,P = f . g ... z,其中所有这些内部函数都可以进行单元测试,因为它们没有依赖关系,只有 P 必须作为集成测试。但是如果在这个内部函数列表中有重复的模式,比如 f . g . f . g,那么你希望将该模式提取到自己的函数中,即 h = f . g,这样就无法进行单元测试了。在插件示例中,如果多个端点需要执行 :validate_is_admin 和 :coerce_form_data 怎么办? - Jesuspc
我可以在单元测试中为这些插件覆盖多种情况 :) - PatNowak
1
由于h的复杂度大于其组件的复杂度,黑盒测试这些函数并不具有可扩展性。如果这种类型的函数不是很常见,那么这是可以接受的,但我的假设是它们很常见。总的来说,我认为你的方法是朝着正确方向迈出的一步,我肯定会在插头中使用你的技巧。谢谢@PatNowak! - Jesuspc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接