4

I am currently struggling to handle config values in mix (particularly when running tests). This is my scenario:

  • i have a client library, with some common config values (key, secret, region).
  • i want to test what happens when there's no region value setup
  • i have no test.exs file in /config

I'm currently doing it like this (and this doesn't work). Module being tested (simplified):

defmodule Streamex.Client do
  @api_region Application.get_env(:streamex, :region)
  @api_key Application.get_env(:streamex, :key)
  @api_secret Application.get_env(:streamex, :secret)
  @api_version "v1.0"
  @api_url "api.getstream.io/api"

  def full_url(%Request{} = r) do
    url = <<"?api_key=", @api_key :: binary>>
  end
end

Test:

setup_all do
  Streamex.start
  Application.put_env :streamex, :key, "KEY"
  Application.put_env :streamex, :secret, "SECRET"
  Application.put_env :streamex, :secret, ""
end

What happens when running mix test is that the main module, which sets attributes from those values, throws the following error since it can't find valid values:

lib/streamex/client.ex:36: invalid literal nil in <<>>

I'm still starting so this may seem obvious, but i can't find a solution after reading the docs.

sixFingers
  • 1,285
  • 8
  • 13

1 Answers1

6

The problem is that you're storing the return value of Application.get_env in a module attribute, which is evaluated at compile time. If you change the values in your tests, it won't be reflected in the module attribute -- you'll always get the value that's present when mix compiled that module, which includes evaluating config/config.exs and all the modules that mix compiled before compiling that module. The fix is to move the variables that can be changed to a function and call those functions whenever they're used:

defmodule Streamex.Client do
  @api_version "v1.0"
  @api_url "api.getstream.io/api"

  def full_url(%Request{} = r) do
    url = <<"?api_key=", api_key :: binary>>
  end

  def api_region, do: Application.get_env(:streamex, :region)
  def api_key, do: Application.get_env(:streamex, :key)
  def api_secret, do: Application.get_env(:streamex, :secret)
end

Note that if this is a library and you want the users of the library to be able to configure the values in their config files, you have to use function calls at runtime as the dependencies of an app are compiled before the app.

Dogbert
  • 212,659
  • 41
  • 396
  • 397
  • Not sure if following SO policies, but i would like to open a bounty and give that to your answer - which already nailed it. To get the prize, it would be cool for me and others if you expanded on the last point you're making, more precisely: by using function calls to read config values, i'm actually forcing my users to _always_ use configuration. Injection of those values would be impossible by other means. This could be problematic when running asynchronous tests. Is this idiomatic in your opinion? Thanks so much. – sixFingers Aug 01 '16 at 15:51
  • "Injection of those values would be impossible by other means." What other means? `Application.put_env` would work perfectly fine. (There's no need for any bounty -- I have enough already. :)) – Dogbert Aug 01 '16 at 15:57
  • If i run async tests, and put an `Application.put_env` in one of them, can i still rest sure that i'll be reading the right config value in every test? – sixFingers Aug 01 '16 at 15:58
  • Yeah, that'll be a problem in async tests. get_env/put_env are similar to global variables in other languages. You would probably need to accept an extra optional config map in each function that requires access to api key/secret/region and use the value present in that, falling back to get_env. – Dogbert Aug 01 '16 at 16:02
  • Thank you so much, you made clear a lot of things. I would upvote again if possible :) – sixFingers Aug 01 '16 at 16:04
  • Some parts of my answer are possibly wrong: it looks like it's possible to read main app's config from a dependency with some force recompiling hacks like https://github.com/elixir-lang/plug/blob/9e70b7713cc963ccfd17ae8f100ce5615b6dbadc/lib/plug/mime.ex#L18-L19. I'll update that part of my answer when I get a chance to investigate. – Dogbert Aug 01 '16 at 16:07
  • Last time I had similar issue. Upvote from my side as well. Nicely done Dogbert. – PatNowak Aug 01 '16 at 18:53
  • @Dogbert Just to add a final note, I ended up with "if not specified, read the value from config" approach. I feel like messing with compilation is not as elegant as allowing injection of values. I'm now having a client structure with key, secret and region fields which gets passed all around. This makes tests clearer in my opinion, while ensuring async coherence. Still it's interesting to know if there are some hacks around. – sixFingers Aug 01 '16 at 20:35