neuone

neuone

Overview

How to test my JSON API controller when a module inside of it relies on making a 3rd party api call.

Use the example from Bypass

I found an example of using Bypass. From the github repo they show an example of unit testing a Twitter client.

Controller Test

Take that example from the github repo of a TwitterClient and let’s create a controller and nest the TwitterClient inside of it. So that every time you visit the endpoint it sends out a tweet. Anytime I visit /api/tweet my controller will TwitterClient.post_tweet/1 and return the tweet we get back from twitter and render some json.

Controller

defmodule MyAppWeb.Api.TweetController do
  use MyAppWeb, :controller
  action_fallback MyAppWeb.Api.FallbackController

  def index(conn, params) do
    with {:ok, result} <- TwitterClient.post_tweet("Elixir is awesome!") do
      render(conn, "show.json",  tweet: result)
    end
  end
end

TestController

defmodule MyAppWeb.Api.TweetControllerTest do
  use MyAppWeb.ConnCase

  describe "GET /tweet" do
    test "success, it sends a tweet", %{conn: conn} do
      conn = get(conn, "/tweet")
      assert json_response(conn, 200) == %{tweet: "Elixir is awesome"}
    end
  end
  
end

If I run this test "GET /tweet" the module TwitterClient is going to hit the live twitter website.

Question

How do I test this controller, but prevent the TwitterClient which I don’t own from actually making the live call to Twitter?

Within my test controller, am I supposed to intercept external HTTP request?

Do I need to read up on Mox?

Any direction, material articles would be appreciated.

Showing Posts 1 to 10

trisolaran

trisolaran

How do I test this controller, but prevent the TwitterClient which I don’t own from actually making the live call to Twitter?

I think you have essentially 2 options, and it all boils down to configuration:

  1. Make your TwitterClient configurable, so that in the test environment it will be started/configured with an endpoint URL pointing to the local Bypass endpoint.

  2. Use Mox and make sure, again via configuration, that your TweetController uses the mocked version of TwitterClient in testing.

If you choose option 1, you will be testing both the controller and the client when testing the controller, and your test will be more “integration-like”. If you choose option 2, your test will be more isolated (you’ll be testing only the controller).

crispinb

crispinb

Bear in mind you can only use Mox if your 3rd party TwitterClient defines a @behaviour. Otherwise you’ll need to investigate other mocking libraries, or mock ad hoc in your tests. Here’s a useful article on the Mox approach: https://medium.com/flatiron-labs/elixir-test-mocking-with-mox-b825a955143f

neuone

neuone OP

crispinb. Great article. This is helpful. Although I am overwhelmed reading docs on bypass and mox.

neuone

neuone OP

trisolaran
With option 1 I can’t figure from my controller test, how I would pass down the Bypass endpoint when my controller test is running.

The configuration is what I’m struggling with. With unit tests it’s easy because you can pass it right in. On a controller test I don’t see how I can pass in the Bypass endpoint from set up.

I think I’m going to go with option 2 with Mox and see how far it takes me.

trisolaran

trisolaran

I think that using Mox (or some other mocking library) makes sense in this case because, as you said, you don’t own the TwitterClient, and so it’s not your job to test its implementation.

But to answer your question about the configuration: looking at the example on GitHub that you shared, I see that the TwitterClient has to be started with a url option. Assuming that you’re starting it in your application’s supervision tree, you can do something like this:

in your lib/myapp/application.ex:

def start(_type, _args) do
    children = [
      {TwitterClient, Application.get_env(:myapp, TwitterClient)}
      ... other apps here
    ]

    opts = [strategy: :one_for_one, name: DataIngestion.Supervisor]
    Supervisor.start_link(children, opts)
  end

in your config/config.exs:

config :myapp, TwitterClient, url: REAL_TWITTER_API_ENDPOINT

in your config/test.exs:

config :myapp, TwitterClient, url: BYPASS_ENDPOINT

Where BYPASS_ENDPOINT is the local endpoint your bypass instance will be listening to. Then you can start Bypass and set up the required expectations before testing the controller.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

You can simply wrap the TwitterClient module in a module of your own that defines a behavior, you don’t need the library to do so.

crispinb

crispinb

True. Tastes vary on this, but while I find it reasonable (advisable!) to refactor towards testability, I wouldn’t myself choose to refactor just to use a specific mocking library. Of course there might be good reasons to do so in particular cases.

neuone

neuone OP

I like this idea of configuration through config.exs and test.exs.

Revising the example

Based on your feedback, the new set up for the controller would use the @endpoint attribute and it would return a url based on the environment that it’s in.

controller(revised)

defmodule MyAppWeb.Api.TweetController do
  use MyAppWeb, :controller
  action_fallback MyAppWeb.Api.FallbackController

  @endpoint Application.get_env(:myapp, :url)

  def index(conn, params) do
    with {:ok, result} <- TwitterClient(@endpoint).post_tweet("Elixir is awesome!") do
      render(conn, "show.json",  tweet: result)
    end
  end
end

How do I test this controller in my test controller?

test controller

defmodule MyAppWeb.Api.TweetControllerTest do
  use MyAppWeb.ConnCase

# I just need to put this here and it will work??
 setup do
    bypass = Bypass.open()
    {:ok, bypass: bypass}
  end

  describe "GET /tweet" do
    test "success, it sends a tweet", %{conn: conn, bypass: bypass} do
      # Is `bypass` being passed here the same bypass
     # that is in my config :myapp, TwitterClient, url: BYPASS_ENDPOINT
       Bypass.expect(bypass, fn conn ->
            conn
            |> Plug.Conn.put_resp_header("content-type", "application/json")
            |> Plug.Conn.resp(200, %{tweet: "Elixir is awesome"})
      end)
      # controller
      conn = get(conn, "/tweet")
      assert json_response(conn, 200) == %{tweet: "Elixir is awesome"}
    end
  end
  
end
dimitarvp

dimitarvp

To alleviate the risk of falling into the trap of wishful thinking that mocking incurs, just use curl (or Postman, Insomnia etc.) to download responses from the actual live API, save them on your machine and then make text fixtures out of them. Something like the following (or you can save them in CSV / XML / JSON and File.read! them inside the mocking functions):

test "whatever" do
  # You should obviously configure a special test implementation that uses `Mox`.
  # This is well-documented elsewhere.
  expect(ThirdPartyApi.Mocked, :get_exchange_rates, fn %{from: "USD"} ->
    # put stuff in here that you got from the actual live / production API.
  end

  assert your_function_calling_the_3rd_party_api() == :desired_result
end

Using Mox is an advantage because it forces you to think of the contract of whatever you want to mock. @benwilson512 alludes to this by advising you to still contract your 3rd-party-consuming modules through using your own behaviour even if the original 3rd party library doesn’t offer one.

You’ll find that as you are Mox-enabling your tests (so to speak) you’ll dig up additional insights along the way. That’s valuable.

crispinb

crispinb

Excellent advice. This also encourages you to keep your mocks thin, and makes use of the API data easy to expand as needs dictate. Maybe this is just me, but I have a tendency to pluck the few values from the API return I think I need for my model, only to find later there’s gold there that I missed. With raw api returns available locally, everything’s always there to be mined later.

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