How to use `mox` to define `Tesla` mocks

Hey everyone, I would like some guidance on how to abstract my current logic around mocking Tesla HTTP requests, using the mox library.

Currently, I’m defining separate modules for mocking, directly using Tesla.Mock.mock. Refer to this module.

I found that this is too repetitive, and difficult to read when you have many modules, and also I’m struggling with a CI error regarding how ExUnit is being loaded: cannot use ExUnit.Case without starting the ExUnit application, please call ExUnit.start() or explicitly start the :ex_unit app

For reference, here’s the full PR if you’d like to take a better look at the tests: [WIP] Major release 1.0.0 by LauraBeatris · Pull Request #52 · workos/workos-elixir · GitHub

With that said, how could I use mox to abstract that mocking logic? I’m still ramping up in my Elixir knowledge so struggling a bit to property understand the library usage around behaviors.

Hi, @LauraBeatris!

About:

cannot use ExUnit.Case without starting the ExUnit application

I see you use ExUnit.Case in your test/support/**_mock.ex files.
Those files are being compiled before test suite starts running. ExUnit.start() is declared in test_helpers.ex

If I understand correctly, you use ExUnit.Case in order to be able to call macros assert macros in the file. We can do that by simply importing.

So in those test/support/*_mock.ex files replace use ExUnit.Case with

import ExUnit.Assertions, only: [assert: 1]

About mocking: seem like you want to “swap” the real Client module with the “mock” in tests. In that case I can’t think of the situation off the top of my head why you would need both - just follow the guide in Mox docs. So in dev/prod your app will use a real module that uses Tesla and makes request, while in test it would use your Mock and expect on functions.


Speaking of Mox+Tesla, I’d like to mention that at my workplace we are mocking Tesla Adapter with Mox using using this approach Deprecate Tesla.Mock · Issue #241 · elixir-tesla/tesla · GitHub

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)[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:

  • Actions
  • Calculations
  • Data

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:

  • Actions - things that make stuff happen, you don’t “own”, potentially hard to test, could break in prod while all other tests pass
  • Calculations - Input in, output out, always the same result. The more of these, the better.
  • Data - what we operate on, the ingredients in all of this

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:

  • Your domain is “HTTP requests” in this case
  • Create a context module called Requests or HTTPRequests
  • Create a behaviour: This is personal preference, but I prefer appending behaviour to Behaviour modules and also adding it to file names. I’ve found it to help folks navigate the code better, especially less senior folks. In this case: RequestsBehaviour
  • Create an implementation (impl) that implements the behaviour: RequestsTeslaImpl/RequestsReqImpl (I like Req :heart:)
  • The Requests module chooses which impl to pick (you can have multiple… this is what makes this important: you can switch them as you need.
  • Set up Mox for that behaviour
  • In your tests, you can enjoy Mox, add your expects as usual

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 :heart:

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!

4 Likes

Not sure I get your problem statement completely – tests are inherently rather unpleasant copy-paste spaghetti when it comes to mocking, sadly. Though it can be partially solved by having maps and then generating code for tests with for, that works too.

Your linked PR is way too big for us to be able to extract the right context quickly IMO.

Can you give an example with some code – or links inside the PR – about what you find repetitive and difficult to read?

1 Like

tests are inherently rather unpleasant copy-paste spaghetti when it comes to mocking, sadly

I do agree that tests require a certain level of repetition.

Your linked PR is way too big for us to be able to extract the right context quickly IMO.

I agree with you and thanks for calling that out. Linking the whole PR wasn’t a good idea since this is a release branch. I should’ve linked only the test snippet.

I see you marked one answer as the solution. Can you show us how you fixed your problem?