Load module during Test

I’m writting tests for a simple module which do HTTP requests.
As recommanded in http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/ I use an environnement variable instead of mocking:

defmodule FaLogentriesParserEx do
  @logentries_api Application.get_env(:fa_logentries_parser_ex, :logentries_api)
end

Of course, for dev and prod environment, the module to load in under lib/fa_logentries_parser_ex but since I don’t want test stuff under lib/ I created the “mock” module under test/fa_logentries_parser_ex/logentries_api_fake.exs

My problem: mix test can’t find that module:

  1) test handle_pipeline_status (FaLogentriesParserExTest)
     test/fa_logentries_parser_ex_test.exs:9
     ** (UndefinedFunctionError) function FaLogentriesParserExTest.LogEntriesApiFake.fetch/2 is undefined (module FaLogentriesParserExTest.LogEntriesApiFake is not available)
     stacktrace:
       FaLogentriesParserExTest.LogEntriesApiFake.fetch(1501576495019, 1501576555019)
       (fa_logentries_parser_ex) lib/fa_logentries_parser_ex.ex:10: FaLogentriesParserEx.fetch/1
       test/fa_logentries_parser_ex_test.exs:89: (test)

Could you help me to make that module loadable or advice a better way to test?

1 Like

Here is the way I did it with a behaviour:

1.) In the mix.exs use elixirc_paths:

  def project do
    [app: :coffee_fsm,
     version: "0.1.0",
     elixir: "~> 1.4",
     elixirc_paths: elixirc_paths(Mix.env),
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    # Specify extra applications you'll use from Erlang/Elixir
    [extra_applications: [:logger]]
  end

  defp elixirc_paths(:test), do: ["lib","test/support"]
  defp elixirc_paths(_), do: ["lib"]

essentially this adds the “test/support” directory where the mock files are

2). in config/config.exs:

case Mix.env do
  :test ->
    config :coffee_fsm, hw: HwMock
  _ ->
    config :coffee_fsm, hw: HwOutput
end

Essentially this will cause test\support\hw_mock.ex to load for testing through @hw_impl in lib\hw.ex - otherwise lib\hw_output.ex is loaded.

3). Meanwhile lib\hw.ex looks like this:

defmodule Hw do
  @hw_impl Application.fetch_env!(:coffee_fsm, :hw)

  defmodule Behaviour  do
    @callback display(f,a) :: :ok when f: String.t(), a: [any()]
    @callback return_change(n) :: :ok when n: non_neg_integer()
    @callback drop_cup() :: :ok
    @callback prepare(t) :: :ok when t: atom()
    @callback reboot() :: :ok
  end

  @spec display(s,a) :: :ok when s: String.t(), a: [any()]
  def display(str, args), do: @hw_impl.display(str, args)
  @spec return_change(n) :: :ok when n: non_neg_integer()
  def return_change(payment), do: @hw_impl.return_change(payment)
  @spec drop_cup :: :ok
  def drop_cup, do: @hw_impl.drop_cup()
  @spec return_change(b) :: :ok when b: CoffeeFsm.beverage()
  def prepare(type), do: @hw_impl.prepare(type)
  @spec reboot :: :ok
  def reboot, do: @hw_impl.reboot()

end 

while test\support\hw_mock.ex looks like this:

defmodule HwMock do
  @behaviour Hw.Behaviour

  use GenServer

  defp forward_pending(pending, pid) when is_pid pid do
    forward_to =
      fn(request, _) ->
        Kernel.send pid, request
        :ok
      end

    pending
    |> Enum.reverse()
    |> Enum.reduce(:ok, forward_to)
  end

  def handle_cast(entry, {:none, pending}) do
    {:noreply, {:none, [entry | pending]}}
  end
  def handle_cast(entry, {pid, pending}) do
    forward_pending [entry | pending], pid

    {:noreply, {pid, []}}
  end

  def handle_call({:forward, pid}, _from, {_, pending}) when is_pid pid do
    forward_pending pending, pid

    {:reply, :ok, {pid, []} }
  end
  def handle_call({:forward, _}, _from, {_, pending}) do
    {:reply, :ok, {:none, pending} }
  end
  def handle_call(:clear, _from, {pid, _}) do
    {:reply, :ok, {pid, []} }
  end

  defp log(entry) do
    GenServer.cast __MODULE__, entry
  end

  def init(_args), do: {:ok, {:none, []}}

  # public interface
  def start_link, do: GenServer.start_link __MODULE__, [], name: __MODULE__

  def clear(pid) do
    GenServer.call __MODULE__, :clear
  end

  def forward(pid) do
    GenServer.call __MODULE__, {:forward, pid}
  end

  # implementing "Hw"" behaviour
  def display(str, args) do
    log { :display, [str, args] }
    :ok
  end

  def return_change(payment) do
    log { :return_change, [payment] }
    :ok
  end

  def drop_cup do
    log { :drop_cup, [] }
    :ok
  end

  def prepare(type) do
    log { :prepare, [type] }
    :ok
  end

  def reboot do
    log { :reboot, [] }
    :ok
  end

end
4 Likes

Thanks a lot peerreynders, elixirc_paths was perfectly what I needed. I’ll check your way to go with behavior, too.