William Ross
William Ross

Reputation: 3910

Elixir mock only one function from a file

I have a test case where I need to mock downloading an image. The issue is when I mock this download function, it makes the other functions in that file undefined, but I also need to call the other functions in the test as they originally exist without mocking.

Is there a way to mock only one function from App.Functions in the example below and keep the rest of the functions working the same?

The code looks like this for setting up the mock:

  setup_with_mocks(
    [
      {App.Functions, [], [download_file: fn _url -> :ok end]}
    ],
    context
  ) 

Upvotes: 0

Views: 1640

Answers (2)

Everett
Everett

Reputation: 9558

Sometimes when you encounter difficulty in mocking functions for testing it can indicate an organizational problem in your code, e.g. a violation of the single-responsibility-principle. Pondering things like this starts to venture into more philosophical territory (which Stackoverflow is not geared towards), but generally it's helpful to isolate your modules in a way that is compatible with testing -- some of the common code/repo organizational patterns fall into place more easily when giving due consideration to facilitating testing.

As already noted, Mock allows the passthrough option. The Mox package does not have a viable solution to this particular use case -- even its skipping-optional-callbacks option does not really fit the bill.

Another option is to go the more manual route: pass an opt (or read one out of the Application config) that can be overridden at runtime to facilitate testing. This tactic smells to me a bit like Javascript's heavy reliance on passing callback functions, but it can work in a pinch, e.g. something like:

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

# OR
def download(url) do
   http_client = Application.get_env(:myapp, :http_client, HTTPoison)
   http_client.get(url)
end

Then in your tests:

  test "download a file" do
     assert {:ok, _} = MyApp.download("http://example", client: HttpClientMock)
  end
  # OR... 
  setup do
    starting_value = Application.get_env(:myapp, :http_client)

    on_exit(fn ->
      Application.put_env(:myapp, :http_client, starting_value)
    end)
  end

  test "download a file" do
      Application.put_env(:myapp, :http_client, ClientMock)
      # ...
  end

This has the disadvantage of punting compile-time errors into runtime (which might be a worthwhile tradeoff to achieve test coverage), and this approach can become disorganized, so use with care.

Generally, I've found Mox's approach to rely on behaviours/callbacks to lead to cleaner tests and cleaner code, but your mileage and use-cases may vary.

Upvotes: 0

ema
ema

Reputation: 5773

Seems that you are using Mock (https://hexdocs.pm/mock/Mock.html). In that case you can use the passthrough option:

test_with_mock "test_name", App.Functions, [:passthrough], [download_file: fn _url -> :ok end] do
        
end

I don't know if the option is available also for setup_with_mocks. More info here: https://github.com/jjh42/mock#passthrough---partial-mocking-of-a-module

Upvotes: 1

Related Questions