Reputation: 4661
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
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
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
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