Mocking system environment variables or alternative

I working on a custom mix task (let’s call it mix update_stuff) that assumes the presence of an environment variable (let’s call it FILE_PATH) that holds a path to a file.

When I test the task, I wish to set the path to a temporary value that is specific to a test. I use ExUnit TmpDir for that, more specifically, I create a temporary file inside tmp_dir and then I wish to set the environment variable only for the context of the test to that file.

I’m not sure how to go about this with ExUnit. I come from Ruby world, where it’s possible to simply stub the value on a test level.

The problem I have seems like a generic problem that most probably many engineers have experienced before, but I can’t find a forum post or a blog post with a solution.

You can use Application.put_env/4. Where you put this depends on the test. If it’s ok to exist for the entirety of your test runs you can throw it in test_helper.ex. Otherwise you can put it in a setup with an on_exit callback calling Application.delete_env/3 to clean it up.

Yes, in Ruby it’s super easy to stub stuff, but it’s not necessarily good. In this case a suggestion to use Application.put_env/4 is of course good, but you are losing the ability to run tests async. Also, I would argue this is not very Elixir-ish way to do things.

I know it’s slightly off-topic, but perhaps you don’t need to keep the file name in an env variable? Environmental variables are to keep things about the environment. I know they are widely used in Ruby as a hack to pass data to commands, but copying tricks from other languages is rarely the best way to do things. You could, for example, just pass the file name as an argument to a mix tasks and the testing this would be trivial.

1 Like

@katafrakt is right. I was way too hasty and not very thorough in my response.

Since it is a mix task, you should be ok using Application.put_env assuming you only have one test file and that this is the only task that cares about that ENV var. I would never suggest doing this for application code. Otherwise, I agree with @katafrakt and think it’s better to pass it as an arg if you’re example is not contrived and you’re really talking about a filename. Passing args to Mix is much nicer than it is in Rake. They just work like normal command line args since Mix doesn’t do that weirdness Rake does where each argument is a different task (running multiple tasks in one go is available in Mix via mix do).

Just a note, Application.put_env/4 deals with application environment (i.e. what you have configured), System.put_env/2 deals with OS environment variables.

lol, yes, right. I’m still taking a lot of assumed knowledge for granted here. I’m gonna take a rest :upside_down_face:

Thanks everyone for thoughtful responses.

As some of you suggested the solution I’m using at the moment involves System.put_env and on_exit. As you’ve pointed out it doesnt’s seem right to go that route, especially because the value is global.

I have three tests that are run async for testing the task, so there is a risk of running into a race condition after repeated runs, it hasnt happened so far during development though.

To give you a bit more context, on my first attempt I implemented a solution where I passed the value directly into the mix task ‘mix update_stuff file/path.txt’, however, I wished for a bit more ergonomic task API, where I store a path into .envrc and load it with dotenv. That way I only need to remember to set the path once and then use the the task simply as ‘mix update_stuff’.

It’s a personal preference, not a hard requirement. It lead me down the path of exploring how would elixir way of achieving such mix task API look like. Perhaps you’ll tell me that putting such arguments into env variables isnt the elixir way indeed. It would be a bit of shame though! :slight_smile:

If the path to the file is truly different in different deployment environments, then I think storing it in a env var is fine. Your actual code should reference the app config, rather than the system var. Your config can then read that var from system in prod, but hard code it in test.

# prod

config :your_app, mix_file_path: System.get_env("FILE_PATH")

# test

config :your_app, mix_file_path: "/path/to/dummy/file"

That’s all you need if the code doesn’t modify the file. If it does, then you just need to create it fresh in the setup for your test.

The simplest way I can think of to accommodate that is to make the path in ENV the default while the task still accepts an argument.

This can solve the testing problem: the tests can pass an explicit argument and not share state, but the everyday user doesn’t have to repeat themselves.

2 Likes

I personally don’t think it’s bad. I was picturing you piping it in like this:

$ FILE_PATH=some/path mix task

…which is something I used to do in Rake because rake "task[some/path]" is pretty gnarly.

But if it’s a local task you run a bunch then I would want it in an ENV var as well.

^ This pattern should be avoided, as the FILE_PATH env var will be read when the code is compiled for production, not in the running production environment itself—for example, using releases, or docker to build your app. runtime.exs should be preferred.

1 Like

I didn’t specify where this config was set, but yes, runtime config in general should be preferred, and the syntax is the same. Nothing prevents you from reading system vars at runtime.

After giving it some thought, this one seems like the most elegant solution.

Thanks everyone who took the time to share your knowledge, much appreciated, I didn’t expect so much feedback, you are a lovely community!

2 Likes