模拟功能灵药

时间:2017-10-25 15:31:33

标签: testing elixir

我有这样的功能

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)函数?

3 个答案:

答案 0 :(得分:4)

您可以更改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

答案 1 :(得分:2)

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

我总是遇到这样的情况:

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的另一个好处是,您可以将所有外部依赖项放在一个位置列出。对于项目中的新人来说,这将更加明确。

答案 2 :(得分:0)

我会使用MecksUnit(我写的一个Hex包),因为我反对为了嘲笑而修改(“暴露”)代码。

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

尽管MecksUnit使用:meck(如果您想尽可能地不引人注目,这是不可避免的),但它通过对每个模块-功能-arity组合仅模拟一次来尝试“尽可能经济”。

取自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