【问题标题】:Mock function elixir模拟函数长生不老药
【发布时间】:2019-01-22 19:08:07
【问题描述】:

我有这样的功能

def foo_bar() do
  Enum.reduce_while(
    image_options,
    0,
    fn image_option, _foo ->
      case image_option["destination"] do
        "s3" ->

          case response = Upload.upload_on_s3(foo, bar) do
            {:ok, _} ->
              {:cont, {:ok, "ok"}}
            {:error, _} ->
              {:halt, response}
          end
        _ ->
          {:cont, {:ok, "todo"}}
      end
    end
  )

end

我想在单元测试中测试 foo_bar。如何模拟Upload.upload_on_s3(foo, bar) 函数?

【问题讨论】:

  • 试试这个 -> github.com/jjh42/mock 如果你想创建一个模拟对象或阅读这个 -> blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts 并采取相应的行动
  • 我建议不要使用 Mock。它使用全局名称执行黑魔法,这意味着您 a) 并不真正知道引擎盖下发生了什么(不好),并且 b) 不能再以 async: true 运行您的测试(非常糟糕)。下面的答案,注入依赖,更好,没有任何这些缺点。

标签: testing elixir


【解决方案1】:

您可以更改 foo_bar 以接受依赖项。下面我展示了一个带有默认参数的模块,但您可以省略默认参数,或者如果您愿意,也可以传递一个函数:

def foo_bar(upload_module \\ Upload) do
  Enum.reduce_while(
    image_options,
    0,
    fn image_option, _foo ->
      case image_option["destination"] do
        "s3" ->

          case response = upload_module.upload_on_s3(foo, bar) do
            {:ok, _} ->
              {:cont, {:ok, "ok"}}
            {:error, _} ->
              {:halt, response}
          end
        _ ->
          {:cont, {:ok, "todo"}}
      end
    end
  )    
end

然后,在您的单元测试中,您可以传递您自己的上传模块的假版本以获得您想要的行为。例如:

defmodule BadFakeUploader do
  def upload_on_s3(_foo, _bar) do
    {:error, "bad stuff"}
  end
end

defmodule TestFooBar do
  use ExUnit.Case

  test "does the expected thing" do
    assert whatever == SUT.foo_bar(BadFakeUploader)
  end
end

【讨论】:

  • 这是一个很好的方法。太多的开发人员对 OO 视而不见,无法看到实施之后的概念。
【解决方案2】:

@trptcolin 写了完全有效的答案,但是明确地接受 upload_module 作为参数对我来说有点 hack,因为你故意通过注入模拟来影响工作应用程序的行为。

我总是在这样的情况下:

1. Create a config for such case
# config.exs 
config :my_app, :uploader,
  RealUploader

# test.exs
config :my_app, :uploader,
  MockUploader

2. Write a mock uploader
# mock only public functions

3. Use it as module attribute to don't change the function call.

@uploader Application.get_env(:my_app, :uploader)
# few lines below...

@uploader.upload_on_s3(foo, bar)

这只是风格问题,但我的建议是不要仅仅因为您想模拟依赖项而更改函数签名及其参数列表。使用 config 的另一个优点是,您可以通过将所有外部依赖项放在一个位置来列出它们。项目新手会更清楚。

【讨论】:

  • 很好,但是我该如何处理 MockUploader 的响应呢? @uploader.upload_on_s3 必须返回 {:ok,_} 甚至 {:error,_} 谢谢!
  • 是的,它会 :) 它只是一个带有硬编码值要返回的模块。
  • 我有这个模拟 def upload_on_s3(foo \\ %{}, bar \\ %{}) do {:ok, %{}} end 但是这个函数总是返回 {:ok,%{}} 我甚至可以创建 {:error,%{}} 但我怎样才能匹配错误?
  • 也许你的函数子句顺序错误。如果您的 upload_on_s3 匹配所有内容,想象一下什么时候应该是错误的情况;)
  • 我必须创建两个模拟函数。第一个返回 {:ok},第二个返回 {:error},但名称相同。在我的“主要”代码中,我调用 @uploader.upload_on_s3(foo, bar) 那么我怎样才能得到 {:error} ?
【解决方案3】:

我会使用MecksUnit(我编写的一个十六进制包),因为我反对为了模拟而改变(“暴露”)代码。

相对于Mock,它确实支持异步测试(因为模拟模块是隔离的)并且定义模拟模块更具可读性/优雅。

尽管 MecksUnit 使用 :meck(如果你想尽可能不引人注目,这是不可避免的),它试图通过只为每个模块-功能-数量组合模拟一次来“尽可能经济”。

取自https://github.com/archan937/mecks_unit/blob/master/test/mecks_unit/bar_test.exs的例子:

defmodule MecksUnit.BarTest do
  use ExUnit.Case, async: true
  use MecksUnit.Case

  defmock List do
    def wrap(:bar_test), do: ~w(MecksUnit Bar Test)
  end

  setup do
    {:ok, %{conn: "<conn>"}}
  end

  mocked_test "parallel compiling", %{conn: conn} do
    task =
      Task.async(fn ->
        assert "<conn>" = conn
        assert [:foo, :bar] == List.wrap([:foo, :bar])
        assert ~w(MecksUnit Bar Test) == List.wrap(:bar_test)
        assert called(List.wrap(:bar_test))
      end)

    Task.await(task)
  end
end

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-02-01
    • 2020-03-29
    • 2010-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-19
    • 1970-01-01
    相关资源
    最近更新 更多