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
Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
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
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews