3
votes

I am trying to use the Mocking Library named Mox in my Elixir project, but even tho following the official documentation: https://hexdocs.pm/mox/Mox.html

I can't define a new behavior for my module functions. It gives me the following error when trying to run the test:

** (ArgumentError) module Myapp.MyModule is not a behaviour, please pass a behaviour to :for (mox) lib/mox.ex:210: Mox.validate_behaviour!/1 (mox) lib/mox.ex:198: Mox.defmock/2 (elixir) lib/code.ex:376: Code.require_file/2 (elixir) lib/enum.ex:678: Enum."-each/2-lists^foreach/1-0-"/2 (elixir) lib/enum.ex:678: Enum.each/2

Here's what I've tried so far:

test_helper.exs:

ExUnit.start()
Mox.defmock(Myapp.MockMyModule, for: Myapp.MyModule)

MyModuleTest.exs

defmodule MyModuleTest do
    use ExUnit.Case

    import Mox

    setup :verify_on_exit!

    test "Test status processor in transit with mocked result" do

        Myapp.MyModule
        |> expect (:put_calculated_eta, fn body, shipment_id, authorization_key -> {:ok, "bla", 200} end)

        map = #Some data that fits the function interface

        assert {:ok, "bla", 200} == Myapp.MyModule.update_shipment_eta(map)
    end
end

What I think is the most weird is that in the documentation it does expect modules to be passed for mocking, but in the error it request for behaviours (which I guess should be functions, which I already tried without success... can anyone tell me why my module cannot be mocked please?

Just for clarification, this mocking is for a response of an external api.

Please let me know if I haven't provided enough info.

2

2 Answers

6
votes

What's missing, as the error message conveys, is a Behaviour. Elixir Behaviours (note the british spelling) are akin to 'interfaces' in strongly-typed OOP languages; that is, they describe a set of functions which other modules implement.

For example, this module defines a behaviour with a single callback method, do_work/1, which expects a job in the form of an integer, and returns either {:ok, result}, or {:error, reason}:

defmodule WorkBehavior do
  @callback do_work(job::integer) :: {:ok, integer} | {:error, atom}
end

And these modules implement that behaviour:

defmodule LazyWorker do
  @behaviour WorkBehavior  
  @impl WorkBehavior
  def do_work(job), do: {:error, :too_tired}
end

defmodule HardWorker do
  @behaviour WorkBehavior
  @impl WorkBehavior
  def do_work(job), do: {:ok, job + 42}
end

The end goal of behaviours is to allow you to write modular code, where various implementations can be swapped with ease. With the code above, the worker module can be passed as a parameter, instead of hardcoding it in. Compare:

def closely_coupled do
  HardWorker.do_work(12)
end

vs.

def loosely_coupled(worker) do
  worker.do_work(12)
end

What Mox is asking you to do is define a Behaviour that captures the API for MyApp.MyModule, so that Mox can create a fake version for you to test against. The motivation for this approach, versus the more typical dynamic mocking is explained in this post by José Valim.

Without seeing the code for MyApp.MyModule, I can't tell you exactly what code to write, but in general, you should add @callback tags to it for each method you want to be able to mock. See this page for an intro to typespecs and behaviours (hint: @callback tags use the typespec syntax, so read the whole page).

0
votes

You need to define the behaviour of your Myapp.MyModule module.

If you do not want to write separate modules that just describe the behaviour as suggested in https://stackoverflow.com/a/48475734/438990, then you can just add @callback descriptors for your public fuctions.

defmodule Myapp.MyModule
  @callback put_calculated_eta(String.t()) :: String.t()
  def put_calculated_eta(fiz)
    fiz + "buz"
  end
end

then you can registre the mock with

Mox.defmock(Myapp.MockMyModule, for: Myapp.MyModule)

instead of

Mox.defmock(Myapp.MockMyModule, for: Myapp.MyModuleBehavior)