LauraBeatris

LauraBeatris

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.

Marked As Solved

RudManusachi

RudManusachi

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

Also Liked

pdgonzalez872

pdgonzalez872

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!

dimitarvp

dimitarvp

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?

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
Fl4m3Ph03n1x
Background Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses....
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
Mooodi
Given a string, how can I get access to its character by index? Enum.at("my_string", 2) doesn't work. Or rather, not char, but a substr...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement