Search code examples
elixirelixir-mix

Handling configuration values in mix


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.


Solution

  • 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.