robbplo

robbplo

I ran into this issue the other day and it’s been bugging me ever since. Below is a basic behaviour/implementation, and function which returns a module at runtime. This is the recommended way to decouple your modules when using Mox. It seems to me that this breaks Dialyzer analysis, since no errors are produced with this clearly incorrect typing.

defmodule MyBehaviour do
  @callback get_integer() :: integer
end

defmodule MyImplementation do
  @behaviour MyBehaviour

  @impl MyBehaviour
  def get_integer, do: 100
end

defmodule DialyzerMoxBehaviours do
  @moduledoc """
  Using the recommended implementation of a behaviour for Mox, Dialyzer is unable to infer the type of `get_integer()`.
  By decoupling this module from `MyImplementation`, we lose the typespec information from the behaviour.
  """

  @spec dialyze_me() :: binary
  def dialyze_me, do: "hello" # valid type

  @spec dialyze_me_two() :: binary
  def dialyze_me_two, do: impl().get_integer() # invalid type, but dialyzer does not complain.

  @spec impl() :: MyBehaviour
  defp impl(), do: Application.get_env(:dialyzer_mox_behaviours, :impl, MyImplementation)
end

Am I missing something? I like using Mox but this seems like a major drawback to me. If anyone else has encountered this issue I’m interested in how you’ve solved it.

Showing Posts 1 to 6

LostKobrakai

LostKobrakai

The approach as stated does loose type information, but not because of mox or behaviours, but because of the runtime selection of the used implementation. No static analysis can catch errors based on information not available statically in the code.

If you make impl() return MyImplementation statically or correctly hardcode the typespec return value of it as MyImplementation then dialyzer can help you. But loosening the type information to not return a specific module means dropping to any module(), which is an alias for atom(). There’s nothing to check if the returned module is unknown.

The typesystem dialyzer uses doesn’t allow you to say “but the module returned implements that behaviour”. It’s either one specific module or any module for that typesystem.

Given the typesystem doesn’t help you can however go the “statically known” route. If you don’t need runtime selection you can compile the configured implementation into the module statically.

@impl Application.compile_env(:dialyzer_mox_behaviours, :impl, MyImplementation)
@spec impl() :: module()
defp impl(), do: @impl
robbplo

robbplo OP

I realize that the compiler cannot know which module is being used at runtime, but i was hoping that returning a reference to the behaviour in the spec would give dialyzer the necessary information. The spec info is contained in the behaviour after all, which doesn’t change at runtime.

The compile_env example does indeed work, thanks for that tip! I often use mocks in some tests and the real implementation in others, so this isn’t ideal for me. I suppose it would be possible to change the module attribute at runtime, but that feels like it goes against the whole point of using Mox.

Second solution

Another possible solution could be to move the dynamic resolution into MyBehaviour:

defmodule MyBehaviourSolutionTwo do
  @callback get_integer() :: integer
  @spec get_integer() :: integer
  def get_integer, do: impl().get_integer()

  defp impl(),
    do: Application.get_env(:dialyzer_mox_behaviours, :impl, MyImplementationSolutionTwo)
end

defmodule DialyzerMoxBehavioursSolutionTwo do
  @doc """
  Dialyzer error appears!
  """
  @spec dialyze_me_two() :: binary
  def dialyze_me_two, do: MyBehaviourSolutionTwo.get_integer()
end

There is some more duplication, and something about adding this code into the behaviour itself feels like a crime of code quality, but I think it would get the job done for me.

Third solution

Another thing i tried which seems preferable, but does not work, looks like this:

defmodule MyDispatcherSolutionThree do
  @behaviour MyBehaviourSolutionThree

  @impl MyBehaviourSolutionThree
  def get_integer, do: impl().get_integer()

  defp impl(),
    do: Application.get_env(:dialyzer_mox_behaviours, :impl, MyImplementationSolutionThree)
end

Add another implementation which uses the dynamic module. Does not give an error, even though I’m calling it statically as MyDispatcherSolutionThree.get_integer(). It works only when i explicitly copy the spec:

  @impl MyBehaviourSolutionThree
  @spec get_integer() :: integer
  def get_integer, do: impl().get_integer()

Doesn’t @impl MyBehaviourSolutionThree also inherit the spec information?

LostKobrakai

LostKobrakai

Mox.stub_with can help there.

robbplo

robbplo OP

I only considered stub_with to have some default functionality in the mock… Seems like a great idea to stub with an actual implementation, thanks!

Gustaw

Gustaw

I have a similar issue, but it’s actually the other way around. There is a client with a stubbed implementation or a real one being chosen at compile time. The stub is for testing/dev and always succeeds returning an {:ok, Res} tuple. The real implementation may fail with {:error, Error}. Since Dialyzer is run with the :test env, it returns The pattern can never match the type. in places in which the caller handles error return values. I want to avoid running Dialyzer in :prod env, and I’m not sure how to deal with this issue.

LostKobrakai

LostKobrakai

You could go with the approach the elixir codebase takes:

— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
water
I’m aware of 2 such projects, are there others? git: GitHub - team-alembic/realworld: A fullstack Phoenix LiveView application with back...
New
itzmidinesh
Hi all, I’m working on a Phoenix project where I need to make external HTTP requests and I’m trying to choose between Req and HTTPoison....
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
leoschweizer
Hi, we run different LLM models across a cluster, with each node loading a configurable subset. Model-specific queues let nodes consume ...
New
csmr
Hi all, I’m new here and would like to hear peoples views on my Elixir/Phoenix based game-engine! :smiley: Mutonex gamedesign was finish...
New
Stef-Verniers
Hello I’ve been coding in Elixir for almost two years now because of my current job where we implemented Elixir in our application. But ...
New

Other Trending Topics Top

dominicletz
Hey there, I haven’t seen a list of useful Elixir specific skills for the popular AI tools yet so I’m starting one here with my first. ...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
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
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews