Mox with Applications - to start or not to start

Hello all! I’m in the process of writing an application with a couple moving parts, but unit testing it is making me question everything.

My application supervises three processes:

children = [
        {DynamicSupervisor, strategy: :one_for_one, name: MyApp.SuperSpider},
        {MyApp.Database, name: MyApp.CurrentDatabase},
        {MyApp.SiteAPI, name: MyApp.SiteAPI},
 ]

When the SiteAPI starts, it loads and starts processes based on what is in the database:

def init(opts) do
     super_spider = opts |> Keyword.get(:super_spider, MyApp.SuperSpider)
     db = opts |> Keyword.get(:db, MyApp.CurrentDatabase)
     sites = db |> Database.get_sites()
     spiders = sites |> Enum.reduce(%{}, fn (site, acc) ->
       {:ok, new_pid} = DynamicSupervisor.start_child(
         super_spider, {MyApp.Spider, {site, db}}
       )
       Map.put(acc, site.id, new_pid)
     end)
     {:ok, %{db: db, super_spider: super_spider, spiders: spiders}}
end

Everything as near as I can tell works, except that when I start the unit tests using mix test, the application first uses the real “database” instead of the MockDatabase which is defined in test/test_helper.ex:

  Code.require_file("test/mock_dets.ex") # Mocking dets with ets
  Mox.defmock(MyApp.MockRequest, for: MyApp.Request)
  Mox.defmock(MyApp.MockDatabase, for: MyApp.Database)
  
  Application.put_env(:my_app, :use_delay, false)
  Application.put_env(:my_app, :request, MyApp.MockRequest)
  Application.put_env(:my_app, :database, MyApp.MockDatabase)
  Application.put_env(:my_app, :dets, MyApp.MockDETS)
 
  ExUnit.start()

When I run the unit tests, the Application is started before the Mox(s) are configured, and the real Database is used. What can I do to avoid this?

I’ve tried not starting the application with “–no-start” but this also stops the dependencies (like Mox) from starting.

Is MyApp.Database both a behaviour and an implementation of it? What’s happening inside?

When you’re adding {MyApp.Database, name: MyApp.CurrentDatabase}, to the supervision tree, it means that OTP will attempt to run MyApp.Database.start_link. If it’s not checking if something is mocked, it will just start the real one every time.

@stefanchrobot Thank you for your quick response! MyApp.Database is both a behavior and a stub that routes it through the application environment variable “:database”

defmodule MyApp.Database do

  @doc """
    Create a new database process
  """
  @callback start_link() :: GenServer.on_start()
  def start_link(), do: impl().start_link()
  @callback start_link(_ :: integer) :: GenServer.on_start()
  def start_link(arg), do: impl().start_link(arg)

  @doc """
    Information for adding to supervision trees
  """
  @callback child_spec(term()) :: Supervisor.child_spec()
  def child_spec(arg), do: impl().child_spec(arg)

  # Lots more functions following a similar pattern here...

  defp impl, do: Application.get_env(:my_app, :database, MyApp.FileDB)
end

The whole app is open source, but it’s really messy (I’ve made some poor design/naming decisions) and definitely a work in progress: ~electric/shop_local: apps/product_search/lib/database.ex - sourcehut git

1 Like

It seems that the app is already started by the time the test_helpers.exs is executed. Can you try configuring the database in config/test.exs (config :my_app, :database, MyApp.MockDatabase)?

You’ll need to put the mock definitions into something like test/support/mocks.ex so that they are visible to the compiler.

That should be part of config/test.exs to be available before the application starts.

Thank you both for your replies! I’ve just now had time to work on these tests and here’s what I’ve come up with:

  1. I moved setting the environment variables to config/test.exs and defined the mox in test/support/mocks.ex

  2. I changed MyApp.Application to only start children when the mix.env isn’t :test

    children = if Mix.env() == :test,
      do: [],
      else: [
        {DynamicSupervisor, strategy: :one_for_one, name: MyApp.SuperSpider},
        {Database, name: MyApp.CurrentDatabase},
        {SiteAPI, name: MyApp.SiteAPI},
      ]
  1. I start the children that each tests needs in the test setup, and give them access to one another:
  setup do
    MyApp.MockDatabase
    |> stub_with(MyApp.FileDB)

    {:ok, super_spider} = start_supervised({DynamicSupervisor, strategy: :one_for_one})

    {:ok, db} = start_supervised(Database)
    {:ok, pid} = start_supervised({
      SiteAPI,
      wait_for_load: true,
      db: db,
      super_spider: super_spider,
      spider_opts: [start_frozen: true],
      name: SiteAPI
    })

    MyApp.MockDatabase |> allow(self(), pid)

    pid |> SiteAPI.load()

    :ok
  end

It may not be the best approach. I had to add an option to SiteAPI to not immediately load sites from the database, but instead wait for “load” to be called, in order to have time to setup the allow with Mox. I also wasn’t able to create a mock for DynamicSupervisor that would’ve allowed me to not actually start the children given to it (mostly because it seems that DynamicSupervisor doesn’t implement any behaviors?), so I added another option to start those children (spider_opts) frozen.

Anyway, thank you both again for your help! I’m of course open to any suggestions you have (Overall I’ve found testing really difficult in Elixir!). Outside of that, maybe this post can help someone in the future.

We had a similar case: a GenServer that is a cache which preloads some data on application boot. We’ve added a configurable flag to make it testable/mockable. So we have something like this in config/test.exs:

config :my_app, SomeCache, warm: false