monkeyUser
monkeyUser

Reputation: 4661

Mock function elixir

I have a function like this

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

I want to test foo_bar in unit test. How can I mock Upload.upload_on_s3(foo, bar) function?

Upvotes: 1

Views: 1379

Answers (3)

Paul Engel
Paul Engel

Reputation: 83

I would use MecksUnit (a Hex package I wrote) because I'm against altering ("exposing") code for the sake of mocking.

Opposed to Mock it does support asynchronous testing (because mock modules are isolated) and defining mock modules is much more readable / elegant.

And though MecksUnit uses :meck (which is inevitable if you want to be as unobtrusive as possible), it tries to be "as economic as possible" by only mocking once for every module-function-arity combination.

An example taken from 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

Upvotes: 0

PatNowak
PatNowak

Reputation: 5812

@trptcolin wrote perfectly valid answer, however accepting upload_module as param explicitly for me it's a bit of hack, because you intentionally impact the behaviour of the working app, by injecting the mock.

I always in situations like these do:

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)

It's just a matter of style, but my suggestion is not to change the function signatures and their list of arguments, just because you want to mock the dependency. Another advantage of using config, is that you can list all your external dependencies by putting them in one place. It will more clear for newcomer in the project.

Upvotes: 3

trptcolin
trptcolin

Reputation: 2340

You could change foo_bar to accept a dependency. Below I'm showing a module with a default argument, but you could omit the default, or pass a function instead if you prefer:

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

Then, in your unit test, you can pass your own fake version of the upload module to have the behavior you want. For example:

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

Upvotes: 4

Related Questions