使用 Mox 模拟 hackney

Mock hackney using Mox

我正在学习 Elixir 和 Phoenix 堆栈,我被投入到一个使用 Hackney 模块 (erlang) 进行 http 调用的生产项目中。这里的情况是我想模拟 hackney 的“请求”函数,所以当它被调用时,它 returns 一个特定的响应。到目前为止,我已经 运行 找到了 Mox 库来实现这一点,但是 hackney 是一个 Erlang 模块,而 Mox 抱怨它不是一种行为,我想知道是否有更好的方法来这样做或者可能还有另一个缺少的配置。代码失败的地方是模拟定义的开头:

Mox.defmock(MyApp.MockHackney, for: :hackney) 这只是失败 module :hackney is not a behaviour, please pass a behaviour to :for

我假设我做错了什么(我在这里遵循 Mox 库的文档:https://hexdocs.pm/mox/Mox.html 理解行为是什么,我理解错误,问题是我不知道是否有办法为这个模拟将行为定义“注入”到哈克尼,或者我使用了错误的工具来完成这项工作。任何想法将不胜感激,提前致谢:)

我将与您分享我使用 Mox 模拟各种第 3 方库的方法,当它们没有定义自己的行为时。

首先: 定义您自己的 Elixir 行为,该行为定义与您正在使用的该库中的函数相对应的回调。例如,如果您正在调用 :hackney.get/2,则在您的行为中为该函数定义一个回调。定义一个你可能不会在你的代码中实际使用的行为可能看起来毫无意义,但它为你做了两件重要的事情:

  1. 它可以帮助您记录您正在使用给定模块中的哪些功能,因此这是对该功能进行抽象的第一步(想想如果您突然不得不换掉 HTTP,您可能需要如何重构使用的客户端库)。
  2. 它满足了 Mox 对具有可以通过反射检查的行为的需求。

例如

defmodule MyClientBehaviour do
  @callback get(url :: binary()) :: {:ok, any()} | {:error, any()}
end

请注意,如果确实仅用于测试,您可以在 test/ 目录中定义行为。

其次:调整您要测试的代码,以便您可以在运行时提供模块。有两种常见的方法可以做到这一点:

  1. 通过提供的 opt
  2. 使值可覆盖
  3. 从配置中解析模块。

例如,如果您的代码使用 :hackney 进行调用,例如

def get(url, opts \ []) do
    :hackney.get(url)
end

您可以改为提供覆盖选项,例如

def get(url, opts \ []) do
    client = Keyword.get(opts, :client, :hackney)
    client.get(url)
end

这样,您可以保留默认实现(在您的情况下为:hackney),您现在可以在测试期间传入模拟。通常,您的 test_helper.exs 将声明正在使用的模拟,例如

# test_helper.exs
ExUnit.start()

Mox.Server.start_link([])

Mox.defmock(HTTPClientMock, for: MyClientBehaviour)
defmodule YourTest do
   use ExUnit.Case

  import Mox
  
  setup :verify_on_exit!

  test ":ok something" do
      client =
        HTTPClientMock
        |> expect(:get, fn _ ->
          {:ok, "some response here"}
        end)
    
       assert {:ok, _} = YourModule.get("http://example.com", client: client)
  end
end

如果您调用 :hackey 的地方嵌入太深,无法进行 opts 注入,将其定义为可配置模块会很有用,例如

def get(url, opts \ []) do
    client = Application.get_env(:my_app, :http_client, :hackney)
    client.get(url)
end

在这种情况下,您可以在 运行 测试之前将值放入您的应用程序配置中(或者您可以将其添加到 test.exs 配置中)。这最好在 setup 块中完成,以确保它在测试之前完成,并确保在完成后将其重新设置。

setup do
    http_client = Application.get_env(:my_app, :http_client)

    on_exit(fn ->
      Application.put_env(:my_app, :http_client, http_client)
    end)
end

Everett's 建议可能是您应该遵循的 - 特别是因为 Mox 在 Elixir 中非常受欢迎。

但是,如果您有时间,我建议您查看 Meck,Erlang 的模拟库。它不需要显式 contracts/behaviours,您还可以模拟不存在的函数,这在 TDD 中很有用。有了梅克,你会像这样嘲笑哈克尼:

setup do
  :ok = :meck.new(:hackney)
  :ok = :meck.expect(:hackney, :get, fn "url" -> :ok end)

  on_exit(fn ->
    :meck.unload(:hackney)
  end)
end

test "mocked hackney request" do
  result = :hackney.get("url")
  assert result == :ok
end

当然,它也支持模拟 Elixir 模块和函数。