0
votes

I am new to both mocking and Elixir, and trying to use the mocking library Mox to improve my tests coverage (mocking the dependencies), I was hoping to be able to create tests for most of my most critical status processors, and other calculations that my application need.

So I have been able to use the library, and as a first approach I got this mocked function to test fine:

test "Mocked OK response test" do
    Parsers.MockMapsApi
    |> expect(:parse_get_distance_duration, fn _ -> {:ok, "test_body", 200} end)

    assert {:ok, "test_body", 200} == Parsers.MockMapsApi.parse_get_distance_duration({:ok, "", 200})
end

It is clear that this test is useless, since what I wanted to mock was this NESTED function from within another function, but only got to mock directly the function that was being called in the test.

So, pointing now my actual problem for a more simple test, that illustrates very well the scenario:

def get_time_diff_to_point(point_time) do
            point_time
            |> DateConverter.from_timestamp()
            |> Timex.diff(Timex.now(), :seconds)
            |> Result.success()
    end  

Clearly the Timex.now() will give me a new timestamp each time, making it impossible to test the calcucation successfully without a mock. So the real question is, How do I mock a NESTED function within the actual function being tested? In this case, I expect Timex.now() to give me the same value every time I run my test... Here's what I got so far (clearly not working, but I think illustrates what I'm trying to do:

test "Mocked time difference test" do
    Utils.MockTimeDistance
    |> expect(:Timex.now(), fn -> #DateTime<2018-01-28 20:13:43.137007Z> end)

    assert {:ok, 4217787010} == Utils.TimeDistance.get_time_diff_to_point(5734957348)
end
1

1 Answers

2
votes

The Mox approach makes this distinction explicit: Your current code has a very specific dependency on Timex.now, and cannot possibly be compiled without it.

In your mind, though, your code only has a dependency on being able to find out the current time, and it is this dependency which you wish to mock during the test.

This is the reason that Mox insists on only mocking behaviours: you are forced to specifically identify the external dependencies that must be controlled.

So, to embrace the Mox way, what you need to do is either:

  1. find a time library that provides a behaviour, -or-
  2. wrap Timex.now in a module of your own that implements a behaviour Mox can work with.

For instance, you could write something like:

defmodule TimeProvider do
  @callback now() :: DateTime.t

  @behaviour __MODULE__
  @impl __MODULE__
  def now(), do: Timex.now()
end

Now, instead of calling Timex.now from your get_time_diff... method, you need to use a dependency injection strategy to call out to an implementation of TimeProvider. Two ways for doing dependency injection: use Application.get_env(app, key) to retrieve the module from config (set it to TimeProvider in config.exs, but overwrite it to MockTimeProvider in test), or set up the module as a default parameter that is only ever overridden in tests.

def get_time_diff_to_point(point_time, time_provider \\ TimeProvider) do
            point_time
            |> DateConverter.from_timestamp()
            |> Timex.diff(time_provider.now(), :seconds)
            |> Result.success()
end  

Mox would need the following setup: somewhere in your test_helper.exs:

Mox.defmock(MockTime, for: TimeProvider)

and in your test:

test "time_diff" do
  expect(MockTime, :now, fn -> DateTime.from_unix!(0) end)
  assert get_time_diff_to_point(DateTime.from_unix!(1), MockTimeProvider) == ...
end

Note that in general, mocking time is both hard and important. This is a simple example, where you know that none of the other function calls in the code-under-test is going to be time-dependent, and now() will only be called once: you can treat it as normal mocking: but beware timing while testing in more involved/time aware code.

EDIT:

After a bit more thought, you can DRY up your code and only do dependency injection in one place by adding a single layer of abstraction:

defmodule MyApp.Time do
  def now() do
    time_provider = Application.get_env(:my_app, :time_provider)
    time_provider.now()
  end
  defmodule TimeProvider do
    @callback now() :: DateTime.t
  end
  defmodule DefaultTimeProvider do
    @behaviour TimeProvider
    @impl TimeProvider
    def now(), do: Timex.now()
  end
end

In your config.exs, you'll need:

config :myapp, time_provider: MyApp.Time.DefaultTimeProvider

And in your test setup, you'll need:

Mox.defmock(MockTime, for: TimeProvider)
Application.put_env(:my_app, :time_provider, MockTime)

Which simplifies your example (and, importantly, all other usages) by removing the optional parameter:

def get_time_diff_to_point(point_time) do
  point_time
  |> DateConverter.from_timestamp()
  |> Timex.diff(MyApp.Time.now(), :seconds)
  |> Result.success()
end

Both approaches have merit- experiment with both, and learn which situations work best with each.