adamu

adamu

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?

Showing Posts 1 to 9

RudManusachi

RudManusachi

I like this idea by @benwilson512

adamu

adamu OP

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

LostKobrakai

LostKobrakai

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.

adamu

adamu OP

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

LostKobrakai

LostKobrakai

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

ityonemo

ityonemo

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.

Adzz

Adzz

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.

Adzz

Adzz

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
jtompl

jtompl

Thanks @Adzz. Worked for us.

Our resulting code looks very similar . Posting here for inspiration.

# lib/my_app/date_time.ex
defmodule MyApp.DateTime do
  @callback now :: DateTime.t()

  def now() do
    Timex.now()
  end

  def instance() do
    Application.get_env(:my_app, :date_time_module, MyApp.DateTime)
  end
end

# test/support/date_time_test_helper.ex
Mox.defmock(MyApp.DateTimeMock, for: MyApp.DateTime)

defmodule MyApp.DateTimeTestHelper do
  def setup do
    Application.put_env(
      :my_app,
      :date_time_module,
      MyApp.DateTimeMock
    )

    :ok
  end

  def stub_current_time(now) do
    Mox.stub(MyApp.DateTimeMock, :now, fn -> now end)
  end
end

# test file
  setup do
    MyApp.DateTimeTestHelper.setup()
  end

  test "if it is midday, does something" do
    MyApp.DateTimeTestHelper.stub_current_time(
      Timex.shift(Timex.now(), day: 1)
    )

    # ... do something
  end
— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews