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

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
pragdave
How do I tell Hologram not to escape HTML when substituting into templates? {Textile.render(@product.long_description)} escapes the HTM...
New
rlipscombe
I’d like to create a file with 600 permissions (rw-------), but there’s a race condition if I create it and then change the permissions. ...
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
rlipscombe
When I use Mix.install with a git dependency, using git@github.com:... how do I change the SSH identity used? I’ve got two Github accoun...
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
raa-ziq
Hi everyone :waving_hand: I wanted to share something I’ve been building — NowBlind, a random chat app designed to help people discover ...
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
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
kip
Localize is the next generation localisation library for Elixir. Think of it as ex_cldr version 3.0. The first version will be released ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews