Mocking a function with optional argument

Hello :wave:

I’m trying to mock a module using the Mox package. Inside that module I have a behaviour defined:

  @callback add(number(), number(), [number()]) :: number

As well as implementation for that behaviour:

def add(a, b, rest \\ []), do: [a, b | rest] |> Enum.sum

Now I’m trying to mock the add/3 function which actually can be called as it would be add/2 (which I’m actually doing inside the “app” code:

    @special_module.add(2, 3)

I tried different expects inside the test like:

  defp xxx(_, _, _ \\ []), do: 6

  test "adds two numbers" do
    Test.SpecialModuleMock
    |> expect(:add, &xxx/3)

    assert Myapp.hello() == 6
  end

but none of my ideas works… as the code tried to call add/2 this is exactly the error I’m getting:

  1) test adds two numbers (MyappTest)
     test/myapp_test.exs:9
     ** (UndefinedFunctionError) function Test.SpecialModuleMock.add/2 is undefined or private. Did you mean:
     
           * add/3
     
     code: assert Myapp.hello() == 6
     stacktrace:
       Test.SpecialModuleMock.add(2, 3)
       test/myapp_test.exs:13: (test)

I created a repository where I have a sample mocked module that does not have optional arguments:

Then I added 3rd argument, and it’s failing:

It looks like the cleanest way to deal with this issue would be to be able to define optional arguments inside the @callback attribute(behaviour defined with optional args) - I couldn’t find a way to do it? Is that even possible?

Otherwise, I’m not sure how to write an expect function so that the Mox will honour optionality of 3rd argument?

I can’t be the first person to mock a function with optional arguments :slight_smile: I surely need to be missing something simple?

You have defined only the callback with 3 arguments in the behavior. (And there is no way to define a callback with optional arguments)

Having said that, I think one of the ways to achieve what you want is to define a callback for each arity:

@callback add(number(), number()) :: number
@callback add(number(), number(), [number()]) :: number

I think the reason is that optional arguments are compile time syntax sugar that expand to another function clause.
As this:

def add(a, b, rest \\ []), do: [a, b | rest] |> Enum.sum

Compiles into the equivalent of

def add(a, b), do: add(a, b, [])
def add(a, b, rest), do: [a, b | rest] |> Enum.sum

If I’m correct.

2 Likes

Thanks for responding. I think you have a point regards compiling into two clauses.

It’s interesting as this example is based on Ecto.Repo which provides @callbacks for all arguments (including optional ones), but functions without optional arguments aren’t part of the behaviour. Maybe that’s an opportunity for a PR? :slight_smile: