Mocking os.environ with python unittests

Question:

I am trying to test a class that handles for me the working directory based on a given parameter. To do so, we are using a class variable to map them.

When a specific value is passed, the path is retrieved from the environment variables (See baz in the example below). This is the specific case that I’m trying to test.

I’m using Python 3.8.13 and unittest.

I’m trying to avoid:

  • I don’t want to mock the WorkingDirectory.map dictionary because I want to make sure we are fetching from the environ with that particular variable (BAZ_PATH).
  • Unless is the only solution, I would like to avoid editing the values during the test, i.e I would prefer not to do something like: os.environ["baz"] = DUMMY_BAZ_PATH

What I’ve tried

I tried mocking up the environ as a dictionary as suggested in other publications, but I can’t make it work for some reason.

# working_directory.py
import os


class WorkingDirectory:
    map = {
        "foo": "path/to/foo",
        "bar": "path/to/bar",
        "baz": os.environ.get("BAZ_PATH"),
    }

    def __init__(self, env: str):
        self.env = env
        self.path = self.map[self.env]

    @property
    def data_dir(self):
        return os.path.join(self.path, "data")

    # Other similar methods...

Test file:

# test.py

import os
import unittest

from unittest import mock

from working_directory import WorkingDirectory

DUMMY_BAZ_PATH = "path/to/baz"


class TestWorkingDirectory(unittest.TestCase):
    @mock.patch.dict(os.environ, {"BAZ_PATH": DUMMY_BAZ_PATH})
    def test_controlled_baz(self):
        wd = WorkingDirectory("baz")
        self.assertEqual(wd.path, DUMMY_BAZ_PATH)

Error

As shown in the error, os.environ doesn’t seem to be properly patched as it returns Null.

======================================================================
FAIL: test_controlled_baz (test_directory_structure_utils.TestWorkingDirectory)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/.pyenv/versions/3.8.13/lib/python3.8/unittest/mock.py", line 1756, in _inner
    return f(*args, **kw)
  File "~/Projects/dummy_project/tests/unit/test_directory_structure_utils.py", line 127, in test_controlled_baz
    self.assertEqual(wd.path, DUMMY_BAZ_PATH)
AssertionError: None != 'path/to/baz'

----------------------------------------------------------------------
Ran 136 tests in 0.325s

FAILED (failures=1, skipped=5)

This seems to be because the BAZ_PATH doesn’t exist actually. However, I would expect this to be OK since is being patched.

When, in the mapping dictionary, "baz": os.environ.get("BAZ_PATH"), I repalce BAZ_PATH for a variable that actually exist in my environment, i.e HOME, it returns the actual value of HOME instead of the DUMMY_BAZ_PATH, which lead me to think that I’m definetely doing something wrong patching

AssertionError: '/Users/cestla' != 'path/to/baz'

Expected result

Well, obviously, I am expecting the test_controlled_baz passes succesfully.

Asked By: cestlarumba

||

Answers:

That’s not directly answer to your question but a valid answer either way imo:
Don’t try to patch that (it’s possible, but harder and cumbersome).
Use config file for your project.

e.g. use pyproject.toml and inside configure the pytest extension:

[tool.pytest.ini_options]
env=[
"SOME_VAR_FOR_TESTS=some_value_for_that_var"
]
Answered By: John Doe

So the problem is that you added map as a static variable.
Your patch works correctly as you can see here:

patch actually works

The problem is that when it runs it’s already too late because the map variable was already calculated (before the patch).
If you want you can move it to the init function and it will function correctly:

class WorkingDirectory:

def __init__(self, env: str):
    self.map = {
        "foo": "path/to/foo",
        "bar": "path/to/bar",
        "baz": os.environ.get("BAZ_PATH")
    }
    self.env = env
    self.path = self.map[self.env]

If for some reason you wish to keep it static, you have to also patch the object itself.
writing something like this will do the trick:

class TestWorkingDirectory(unittest.TestCase):
@mock.patch.dict(os.environ, {"BAZ_PATH": DUMMY_BAZ_PATH})
def test_controlled_baz(self):
    with mock.patch.object(WorkingDirectory, "map", {
        "foo": "path/to/foo",
        "bar": "path/to/bar",
        "baz": os.environ.get("BAZ_PATH")
    }):
        wd = WorkingDirectory("baz")
        self.assertEqual(wd.path, DUMMY_BAZ_PATH)
Answered By: Nimrod Shanny