Using Mox to mock DateTime used during init

I’m trying to use Mox to test a fix to a DateTime bug in my app.

The bug was that I was subtracting 1 from the current hour, which was failing at 00h.

I have a GenServer managing access tokens. It sends a message to itself every hour (using Process.send_after) to cleanup expired tokens. Part of this logic uses DateTime to calculate when the next cleanup should be scheduled. It schedules the first cleanup during init().

I want to test that the cleanup code works correctly at 00h, so my first thought was to mock DateTime and fix the date. DateTime doesn’t implement a behaviour, so I wrote my own behaviour + implementation that wraps the subset of DateTime functions I’m using.

The problem I’m having is how to mock this properly. I’m using Mox, and setting up a mock in test_helper.exs. However, because the Genserver uses the DateTime in init(), it tries to call the mock before I can define any expectations.

I tried adding Mox.stub_with at compile time in support/mocks.ex, to just forward to the real implementation for now, however then I get an error that Mox.Server is not started.

Is it possible to use Mox to mock a module when that module is used during application initialization?

Here’s the startup error using stub_with from the tests:

** (Mix) Could not start application my_app: MyApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: MyApp.TokenServer
    ** (EXIT) an exception was raised:
        ** (Mox.UnexpectedCallError) no expectation defined for MyApp.DateTimeMock.utc_now/0 in process #PID<0.259.0> with args []
            (mox 0.5.2) lib/mox.ex:693: Mox.__dispatch__/4
            (my_app 0.1.0) lib/my_app/token_server.ex:64: MyApp.TokenServer.schedule_cleanup/0
            (my_app 0.1.0) lib/my_app/token_server.ex:13: MyApp.TokenServer.init/1

Here’s the error if I try to use stub_with at compile time:

== Compilation error in file test/support/mocks.ex ==
** (exit) exited in: GenServer.call(Mox.Server, {:add_expectation, #PID<0.172.0>, {MyApp.DateTimeMock, :utc_now, 0}, {0, [], &MyApp.DateTime.utc_now/0}}, 30000)
    ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
    (elixir 1.10.3) lib/gen_server.ex:1013: GenServer.call/3
    lib/mox.ex:547: Mox.add_expectation!/4
    lib/mox.ex:476: Mox.stub/3
    lib/mox.ex:526: anonymous fn/4 in Mox.stub_with/2

Finally, a bigger question: Any suggestions for alternative ways to test time bugs like this?

1 Like

I like this idea by @benwilson512

3 Likes

Yeah, that’s probably going to work better than all the mocking. Will refactor it and see how it looks. Thanks.

I tend to not unit tests processes started by the supervision tree, but setup separate instances I test. This also allows those tests to be able to run async.

@LostKobraki That makes sense. You mean calling GenServer.start from inside the tests, then exercising that instance, right?

There’s start_supervised in ex_unit, which will handle proper cleanup automatically.

I think what you want to do is run Mox.stub_with immediately after Application.ensure_all_started(:mox) in test_helpers.exs. IIRC the Application tree is started as part of ExUnit.start

If this doesn’t work, as kobrakai suggests, you can do a thing where your server isn’t started in the Application tree, gated on Mix.env == :test (just remember to make sure it’s a compile time gate and not a runtime instruction); and you can manually start the server in test_helpers.

1 Like

This is usually fine, but you are then never really testing the presence of the default argument. That may be fine for you or not.

I personally wrap DateTime in a helper, so I can make methods like Date.greater_than?

BUT regardless if you mock the actual datetime module and use an expect in the test it will work

def init() do
  ..
  MyApp.DateTimeUtils.utc_now()
 ...
end

Then set up mox as usual:

# test.exs
config(:my_app, date_time_module: DateTimeMock)

# somewhere:
Mox.defmock(DateTimeMock, for: DateTime.Behaviour)

# the behaviour
defmodule DateTime.Behaviour do
    @callback utc_now :: DateTime.t()
end

# the Utils Module
defmodule MyApp.DateTimeUtils do
  @behaviour DateTime.Behaviour

  @impl true
  def utc_now() do
     date_time().utc_now()
  end

  defp date_time(), do: Application.get_env(:my_app, :date_time_module, DateTime)
end
1 Like