How to get rid off a warning about module being undefined when module is in a script .exs file? (from [Programming Phoenix 1.4])

Hello everybody,
I’m following along the Programming Phoenix 1.4 Book.
And at a moment we have the following simple code that do an HTTP request using erlang :httpc (excerpt simplified):

defp fetch_data(url) do
  {:ok, res} = :httpc.request(url)
  res
end

And for testing purpose, we want to mock the HTTP request from :httpc.
But for that we defined in the regular code itself a “conditional” use of the mock instead of the regular library when the code is running in the tests.
So we first defined a config option for the test environment in config.test.exs like so:

# config/test.exs
config :info_sys, :wolfram, http_client: InfoSys.Test.HTTPClient

That it triggers the mock HTTPClient in the module InfoSys.Test.HTTPClient when running in the test environment thanks to the following code in the code (located in a module named Wolfram in the example):

# lib/info_sys/wolfram.ex
@http Application.get_env(:info_sys, :wolfram)[:http_client] || :httpc
defp fetch_data(url) do
  {:ok, res} = @http.request(url)
  res
end

So we’re using :httpc in all environment but the test environment.

Now the mock HTTP client is in an Elixir Script file located somewhere within the test directory:

# test/backends/http_client.exs
defmodule InfoSys.Test.HTTPClient do
  def request(url)
    ...
    data
  end
end

In order to this to work we carefully included the code to be required in top of the test_helper.exs file:

# test/test_helper.exs
Code.require_file("backends/http_client.exs", __DIR__)
ExUnit.start()

Now everything work but I always get the following warning:

warning: InfoSys.Test.HTTPClient.request/1 is undefined (module InfoSys.Test.HTTPClient is not available or is yet to be defined)
  lib/info_sys/wolfram.ex:4 InfoSys.Wolfram.fetch_data/1

Here I put back the code where the warning is located:

# lib/info_sys/wolfram.ex
@http Application.get_env(:info_sys, :wolfram)[:http_client] || :httpc
defp fetch_data(url) do
  {:ok, res} = @http.request(url) # Warning on this line
  res
end

This warning really bothers me and I didn’t find a way to get rid of the warning.
I guess that this is related to the lack of .beam file generated for .exs files.
So I even tried to change the mock module inside a .ex file, but it seems that since it’s in the test folder it didn’t get compiled and that I still to use the Code.require_file to make it work.

Has anyone have any idea on how to clean up this warning?

Thank you very much.

The proper way were to add test/support to :elixirc_path or something and save the module in a proper *.ex file in that folder. Then no Code function is necessary anymore.

You can see an example of how to do that in a phoenix project, they are already generated with that pattern in mind.

1 Like

NobbZ’s suggestion is a good one. Also however this is an anti-pattern. Do the Application.get_env call at runtime. This will also remove your warning.

2 Likes

Indeed…
Doing the following (for example):

# lib/info_sys/wolfram.ex
defp fetch_data(url) do
  http_lib = Application.get_env(:info_sys, :wolfram)[:http_client] || :httpc
  {:ok, res} = http_lib.request(url)
  res
end

Remove the warning, but having this at runtime is not really production-friendly imho…
I guess that Chris, José and Bruce (authors of the book) did like that particularly because of not doing to have that code executed in runtime…

What do you mean when you say “not production-friendly”? If you’re worried about performance fetching data from the application env is just from memory and very fast, likely 10,000 (completely non-scientific estimate) times faster than the network call that you make on the very next line. I would say that it will almost never be a problem.

1 Like

I completely agree about the raw performance concern…

And indeed, within the book we played with caching HTTP requests and as you said fetching from ETS was something like 50µs instead of >1s … So yeah likely 20,000x faster! And I bet it’s even more fro just reading a config value…

But I was more concerned about having to perform something related to tests (so not really a functional requirement) at runtime instead of at compile time, and simply mixing those concerns.
And I’m also not sure if it’s a good idea to adapt the code for testing purposes…
I think it’s better when it goes the other way around, no?

I don’t think fetch_data/1 should know about configuration - it would be better to be passed in as a parameter. For example:

defp fetch_data(url, %{http_lib: http_lib}) do
  {:ok, res} = http_lib.request(url)
  res
end

And pass the config to the function from the business layer further up.

1 Like

There is gonna be an Application.compile_env option that I think makes the line between stuff done at compile time and stuff done at runtime more clear in the next Elixir version. I think in application code my state may be slightly too strong, I think there can be a place for compiling it in. In libraries it’s a real issue, although maybe compile env solves that too.

1 Like

Yes I agree…
But regarding the concern here (being able to mock the HTTP request) I think that doing dependency injection is still not the best. Here the business layer is just the test cases…

I mean it’s not like we had to provide as a functionality the ability to switch between different HTTP implementations, in which case DI makes total sense.

Anyway as I said above bringing in the code changes like that just in order to make the tests working (or not displaying an error) is a no-no for me imho to begin with…

Wow well that’s good to know. Why on earth would they include an antipattern in the official book ?

1 Like

I think I was maybe a bit hasty in my reply. It’s an anti-pattern for libraries, but it works just fine for application code. It’s an anti-pattern for libraries because it is only re-evaluated when the library is compiled, which can mean it can get set and not un-set when config changes. For application code which is always recompiled when config changes it’s less of an issue.

In the general case though, configuration has changed a meaningful amount since earlier versions of Elixir, and some old patterns may still be out there that aren’t ideal.

2 Likes

Fair enough!

Just for completeness sake, could you elaborate on this and what said config changes mean for this specific issue?

1 Like