Home

2023-12-11 | More on Mox (ElixirForum answer)

(Turn your phone sideways if you are on mobile)

This was answered on ElixirForum here https://elixirforum.com/t/how-to-use-mox-to-define-tesla-mocks/60177/2

More on Mox (ElixirForum answer)

Hi! I'd like to answer this question by framing it differently: not how to "mocking Tesla" but about how to define our Requests domain and create a boundary around it.

I like how you used the word abstraction, because that's exactly what we'll address here. In order to do so, I wanted to add some context I find extremely important when discussing these topics. It's the three categories that code falls under in Grokking Simplicity This book is such a fantastic resource by the way, I highly suggest getting a copy. There, Eric Normand mentions the three categories functional programmers group code:

This technique works with whatever Action you are dealing with. On page 10, the author defines an Action as:

Anything that depends on when it is run, or how many times it is run, or both, is an action. If I send an urgent email today, it's much different from sending it next week. And of course, sending the same email 10 times is different from sending it 0 times or 1 time.

In my humble attempt (the book does a better job at this), I summarize them as:

HTTP requests fall in that Actions category. In the very least, if you send a request to an api and they are down shows that it is not a calculation. This is when you start to see an opportunity to create a boundary around this part of the code. If you dig a little deeper, you may see the abstraction and be able to create a "domain". Actions give you hints about your domain and where to set your boundaries. In Elixir/Phoenix land, we have great tools to deal with these boundaries:

Contexts are an excellent way to think about this. It's such a fantastic approach. I don't think there is a mention of the word domain in the Phoenix Contexts docs, but we should likely try to add it. Maybe it's done on purpose to not get so academic about it and avoid bikeshedding. But I personally see Contexts and "domains" as somewhat interchangeable here.

Now, with all of this said, we can put everything together:

Let's walk through it:


# The context
# lib/my_app/requests.ex
defmodule MyApp.Requests do
  def get(args) do
    impl().get(args)
  end

  # this is the switch that chooses which impl to use at runtime
  defp impl do
    Application.get_env(:my_app, :requests_impl)
  end
end

# The behaviour
# lib/my_app/requests/requests_behaviour.ex
defmodule MyApp.Requests.RequestsBehaviour do
  @callback get(map()) :: {:ok, map()} | {:error, map()}
end

# The implementation (impl)
# lib/my_app/requests/requests_hackney_impl.ex
defmodule MyApp.Requests.RequestsHackneyImpl do
  @behaviour MyApp.RequestsBehaviour

  @impl true
  def get(args) do
    args
    |> :hackney.get()
    # you can normalize the output here. Seems like it returns a tuple
    # rather than an {:ok, map()}, which the others are all standard
    |> case do
      {:ok, _status, body} -> {:ok, %{body: body}}
      error -> error
    end
  end
end
Then, you can set up Mox. The readme should get you going, but let's add the steps that leverage config here as well:

# in test/test_helper.exs
Mox.defmock(RequestsBehaviourMock, for: MyApp.RequestsBehaviour

# config/config.exs
config :my_app, :requests_impl, MyApp.Requests.RequestsHackneyImpl

# config/test.exs
config :my_app, :requests_impl, RequestsBehaviourMock
Sorry I didn't address your problem at hand (dealing with the `Tesla.Mock` thing). It felt a little bit like an XY problem to me and a chance to address something I see often. Another thing that I want to address is that the example above uses `hackney`. That's because I didn't write the above today, I wrote it to a friend of mine that asked me about this just last week. I think that once you approach the problem the way I mention above, things become much easier to solve. In your case, with Tesla, the `MyApp.Requests.RequestsHackneyImpl` would likely look like:

# The implementation (impl)
# lib/my_app/requests/requests_tesla_impl.ex
defmodule MyApp.Requests.RequestsTeslaImpl do
  @behaviour MyApp.RequestsBehaviour

  @impl true
  def get(args) do
    Tesla.get(args)
  end
end

Side note: Once you have the set up with the behaviours and such, it should be straight forward to move to Req ❤️

Side note 2: I also don't mean to diss/put down the current approach. I'm not versed in that library, maybe others can chime in to help. It's a non-issue when approaching the problem as above. I hope my answer is not seen this way.

Hope this helps! --------------

Thanks for reading, PDG

Home