ArthurMmn

ArthurMmn

I have a behaviours with lots of functions. Each pass an arguments and an optional keyword list of options. And I found myself in this weird situation where testing with Mox introduce some unsatisying results.

A reduction of the problem :

defmodule MyBehaviour do
  @callback greet(name :: String.t(), greeting :: String.t()) :: String.t()
end

defmodule MyModule do
  @behaviour MyBehaviour

  @impl true
  def greet(name, greeting \\ "Hello") do
    "#{greeting}, #{name}!"
  end
end

And a test like this

import Mox

defmock(MyMock, for: MyBehaviour)

MyMock
|> expect(:greet, fn name -> "Hello #{name}" end)

result = MyMock.greet("World")

It fails because the mock does not know any function of arity 1 called greet.

** (ArgumentError) unknown function greet/1 for mock MyMock
    (mox 1.2.0) lib/mox.ex:681: Mox.add_expectation!/4
    (mox 1.2.0) lib/mox.ex:549: Mox.expect/4

If I test instead

import Mox

defmock(MyMock, for: MyBehaviour)

MyMock
|> expect(:greet, fn name, _ -> "Hello #{name}" end)

result = MyMock.greet("World")

It obviously fails, because the function with one argument is never mocked.

** (UndefinedFunctionError) function MyMock.greet/1 is undefined or private. Did you mean:

      * greet/2

    MyMock.greet("World")

Which leaves me with two choices (I think) :

  1. Duplicating all callback with and without the optional arguments (sad when there is a lot of callbacks)
  2. Removing the “optional” from the arguments and always calling the two arity functions.

I chose option 2 with a lack of enthusiasm, anyone in the same situation chose a different solution ? Or had a better way of dealing with the situation ?

Showing Posts 1 to 10

LostKobrakai

LostKobrakai

You’re essentially running into the fact that behaviours do not have optional parameters. There’s just fixed arity callbacks. Mox being driven by behaviours inherits that. But you could always place an interface with optional parameters in front of code calling only the “fully arity” behaviour callback implementations. That’s e.g. how ecto does it.

E.g.

defmodule Greeter do
  def greet(name, greeting \\ "Hello") do
    Application.get_env(:myapp, __MODULE__).greet(name, greeting)
  end
end
ArthurMmn

ArthurMmn OP

I guessed there was not much more to find here.

Any thoughts on why callbacks were never implemented with optional parameters ?

LostKobrakai

LostKobrakai

Optional parameters are a compile time construct of elixir. Behaviours are an erlang level feature, which existed even before elixir existed. Generally I’d keep the interface to multiple implementations as simple as possible and handle the optional stuff before or after that interface.

garrison

garrison

Expanding on the above, default args are not actually “real”. They are just syntax sugar for additional function heads.

I suppose that sugar could be extended to callbacks, though it might be a bit confusing to generate multiple callback “heads”. The abstraction starts to leak.

And who defines the default arg’s value? The callback, or the implementation? It’s messy.

funboy

funboy

you can try to use GitHub - edgurgel/mimic: A mocking library for Elixir · GitHub
there is no mess with creating behaviours only for testing purposes :smiley:

lud

lud

Would you be able to define a behaviour like this?

defmodule Behaviour do
  # Invalid syntax
  @callback greeting(name :: binary, greeting \\ "hello" :: binary)
end

If yes then there is no built in mechanism to automatically pass the arguments into the implementation modules

defmodule Impl do
  @behaviour Behaviour
  @impl true
  def greeting(name, greeting), do: "#{greeting}, #{name}!"
end

Because the behaviours are not called, your code will call the module directly:

Impl.greeting("World")

So the Impl module must have a greeting/1 function.

You can of course define different functions in the behaviour

defmodule Behaviour do
  @callback greeting(name :: binary)
  @callback greeting(name :: binary, greeting :: binary)
end

And who defines the default arg’s value? The callback, or the implementation? It’s messy.

Yes and there is no mechanism to define default values from outside of a module, so it must be the implementation module.

A pattern I see a lot is using the behaviour as the API:

defmodule Behaviour do  
  @callback greeting(name :: binary, greeting :: binary)

  def greeting(module, name, greeting \\ "Hello") do
    module.greeting(name, greeting)
  end
end
mudasobwa

mudasobwa

Creator of Cure

In my humble opinion, this sentense puts the cart before the horse. Like one never tests private functions, one arguably should not mock anything but behaviours. Mocking arbitrary functions just makes zero sense, simply revealing the design flaw, which would definitely raise later.

funboy

funboy

Interesting point!
Let me explain my use case:
I often use it for unit tests where I want to focus purely on the logic inside a single module. For example, I don’t want to spin up the processes that call HTTP services or the database - those dependencies would each need their own mocks every time I test a simple function like calc/0.

def calc() do
      # requires multiple processes 
      # that perform (via these processes) some HTTP requests and database queries
      points = Foo.points() 
      Enum.sum(points)
end

# A simple unit test example that doesn’t require mocking up half the world 
# or adding a behaviour for Foo module just for testing purposes.
   
   use Mimic
   Mimic.copy(Foo)

   test "... calc the sum of the user’s points" do
      expect(Foo, :points, fn -> [1, 1, 1] end)
      assert 3 == Boo.calc()
   end

For that unit test, I’d rather just mock the return value of Foo.points/0 instead of setting up deeply nested mocks for everything under it or adding a new behaviour solely for test purposes.
of course those dependent processes are already covered by integration test validating that everything works together :smiley:
So in short: Mimic is convenient when I want to test simple logic without introducing extra behaviours or large amounts of test setup with Mox for deeply-nested behaviour based (http/db etc) dependencies
Let me know if I’m missing sth :smiley:

mudasobwa

mudasobwa

Creator of Cure

Well, I might not be the best person to discuss testing techniques with in the first place. I was always loudly against the “test for the sake of tests,” and 100% coverage, and whatnot on that matter.

I have a strong opinion that while unit tests are great during development stage, they are a burden for both regression and refactoring. I never need to constantly validate that calc/0 returns 3. It just makes no sense, because if it did, I’d write it as def calc, do: 3.

What makes sense to test, is …ahem… how your app behaves. What is literally covered by erlang behaviour paradigm. Your Foo.points/0 should not be deadly nailed, because tomorrow you’ll discover a better, faster, or cleaner way to calculate points, and you surely won’t want to overwrite the existing implementation, at least during development and for benchmarks.

Every time I find myself unit-testing a function in the wild, I ask, why, for God’s sake I want to bring a fragile test, depending on my current implementation, to the test suite? The answer is always either “Huh, that’s a behaviour then,” or “I should make this function private and test its implication.”

funboy

funboy

Me too! :smiley:

What makes sense to test, is …ahem… how your app behaves.

100% agree!

I never need to constantly validate that calc/0 returns 3

That unit test checks if calculations are correct - you can refactor calc function later - but the test will stay the same - calc function for same data should return the same output - and testing if calculations are correct it’s worth to check in unit test I guess :thinking:

Maybe a more abstract example would illustrate it better, though it still might be a bit lacking - sorry.

def calc() do
      # requires multiple processes 
      # that perform (via these processes) some HTTP requests and database queries
      data = Boo.data()
      # calc algorithm
      ...
end

test "for dataX should return respX" do
      dataX = ...
      expect(Boo, :data, fn -> dataX end)
      assert respX == Boo.calc()
end

why, for God’s sake I want to bring a fragile test,

In my experience, clean, small, fast, async unit tests are far less fragile than test suites that require dozens of deeply-nested behaviour mocks just to verify some calculation logic/ output :thinking:
but I might be wrong
I see u don’t like unit tests and you have every right to :ok_hand:

Where Next? Top

Trending in Questions Top

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
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
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
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
apz
I’m new to elixir and just tried to install the elixirLS extension for VScode(ium) and it is throwing some errors that I would like help ...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
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
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
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
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

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews