0
votes

I'm learning Elixir and the Phoenix stack and I got dropped into a production project that uses the Hackney module (erlang) to make the http calls. The situation here is that I want to Mock hackney's "request" function so when it's called, it returns a specific response. So far, I've run around and landed on the Mox library to achieve this but being that hackney is an Erlang module and Mox complains about it not being a behavior, I'm wondering if there is a better way to do this or maybe there is another missing configuration. Where the code fails is in the very begining of the mock definition:

Mox.defmock(MyApp.MockHackney, for: :hackney) This just fails with a module :hackney is not a behaviour, please pass a behaviour to :for

I'm assuming I'm doing something wrong (I'm following the documentation for the Mox library here:https://hexdocs.pm/mox/Mox.html Understanding what a behaviour is, I understand the error, the thing is I don't know if there is a way to "inject" the behaviour definition to Hackney for this mock or I'm using the wrong tool for the job. Any ideas would be very much appreciated, thanks in advance :)

2
I think defining a behaviour for your client, which wraps up the calls to hackney is the way to go. Then you can mock that behaviour in your tests. I struggle with that though, because it's not exactly practical to replace all your http calls with your custom behaviour.Adam Millerchip
I thought about that too, but there is a bunch of hackney stuff scattered around on the project, so I just wanted to mock the response of one :hackney.request to add a test and not turn it into a full refactor :(Stoic Alchemist
Probably a bit too open-ended for StackOverflow - try the elixir forum.Adam Millerchip
“there is a bunch of hackney stuff scattered around on the project”—this is already a huge architectural issue and I’d start with fixing this. See hexdocs.pm/boundary/Boundary.html for whats and whys.Aleksei Matiushkin

2 Answers

3
votes

I will share with you an approach I've used to mock various 3rd party libraries using Mox when they don't define their own behaviour.

First: Define your own Elixir behaviour that defines callbacks corresponding to the functions you are using from that library. For example, if you are calling :hackney.get/2 then define a callback in your behaviour for that function. Defining a behaviour that you might not actually be using in your code may seem pointless, but it does 2 important things for you:

  1. it helps you document which functions you are using from a given module and therefore it is the first step in making an abstraction for that functionality (think how you might need to refactor things if you suddenly had to swap out the HTTP client library used).
  2. it satisfies Mox's need to have a behaviour that it can inspect via reflection.

E.g.

defmodule MyClientBehaviour do
  @callback get(url :: binary()) :: {:ok, any()} | {:error, any()}
end

Note you can define the behaviour in your test/ directory if it is indeed only used for testing.

Second: Adjust the code that you would like to test so that you can provide the module at run-time. There are 2 common ways to do this:

  1. make the value overridable via a supplied opt, OR
  2. resolve the module from configuration.

For instance, if your code is using :hackney to make calls, e.g.

def get(url, opts \\ []) do
    :hackney.get(url)
end

You can instead supply an option for overrides, e.g.

def get(url, opts \\ []) do
    client = Keyword.get(opts, :client, :hackney)
    client.get(url)
end

This way, you can keep the default implementation (:hackney in your case), and you can now pass in the mock during tests. Typically, your test_helper.exs will declare the mocks in use, e.g.

# test_helper.exs
ExUnit.start()

Mox.Server.start_link([])

Mox.defmock(HTTPClientMock, for: MyClientBehaviour)
defmodule YourTest do
   use ExUnit.Case

  import Mox
  
  setup :verify_on_exit!

  test ":ok something" do
      client =
        HTTPClientMock
        |> expect(:get, fn _ ->
          {:ok, "some response here"}
        end)
    
       assert {:ok, _} = YourModule.get("http://example.com", client: client)
  end
end

If the places where you are calling :hackey are too deeply embedded to allow for the kind of opts injection, it can be useful to define this as a configurable module, e.g.

def get(url, opts \\ []) do
    client = Application.get_env(:my_app, :http_client, :hackney)
    client.get(url)
end

In this case, you can put the value in your Application config prior to running the test (or you can add it in the test.exs config). This is best done in a setup block to ensure that it gets done prior to the test and make sure to set it back when finished.

setup do
    http_client = Application.get_env(:my_app, :http_client)

    on_exit(fn ->
      Application.put_env(:my_app, :http_client, http_client)
    end)
end
3
votes

Everett's suggestion is probably what you should follow - especially since Mox is very popular in Elixir.

However, if you have time I'd recommend looking at Meck, Erlang's mocking library. It does not require explicit contracts/behaviours, and you can also mock non-existent functions, which can be useful in TDD. With Meck, you would mock hackney like this:

setup do
  :ok = :meck.new(:hackney)
  :ok = :meck.expect(:hackney, :get, fn "url" -> :ok end)

  on_exit(fn ->
    :meck.unload(:hackney)
  end)
end

test "mocked hackney request" do
  result = :hackney.get("url")
  assert result == :ok
end

Naturally, it also supports mocking Elixir modules and functions.