thomaspaulin.me

Testing logic reliant upon "compile-time" calls to os.getenv with default values

In Python we can initialise variables at the top-level in the following way:

1is_production = True

I recently ran into a quirk of this behaviour when trying to mock the return value of os.getenv which had a default value. I was trying to parse a boolean value from an environment variable whose default value was evaluated as follows:

1is_production = os.getenv("IS_PRODUCTION", "true").upper() == "TRUE"

I mocked os.getenv using monkeypatch as recommended by pytest only to discover that using importing at the top-level was insufficient.

1import pytest
2
3from const import is_production
4
5def test_is_production_is_false(monkeypatch):
6    monkeypatch.setenv("IS_PRODUCTION", "false")
7    assert is_production is False
8    # True because the import and therefore the os.getenv(...) default value
9    # was used before monkeypatch set the environment variable

To the horror of many linters I tried to mitigate this by moving the import inside the test function itself.

1import pytest
2
3def test_is_production_is_false(monkeypatch):
4    monkeypatch.setenv("IS_PRODUCTION", "false")
5    from const import is_production
6    # Now monkeypatch patches the environment variable before the call to
7    # os.getenv(...) is executed
8    assert is_production is False

This is ugly if you wish to test different values so I turned it into a parameterised test function

 1import pytest
 2
 3# Note the quotation marks
 4@pytest.mark.parametrize("env_var, expected", [
 5    ("True", True),
 6    ("TRUE", True),
 7    ("tRuE", True),
 8    ("false", False),
 9    ("FALSE", False),
10])
11def test_is_production(env_var, expected, monkeypatch):
12    monkeypatch.setenv("IS_PRODUCTION", env_var)
13    from const import is_production
14    assert is_production is expected

You’ll notice however, that the order of the parameters now matter and can cause test failures when an expected False comes after a True because the import line is evaluated once for the fixture. To solve this we simply need to delete the imported module from the system modules.

 1import sys
 2import pytest
 3
 4@pytest.mark.parametrize("env_var, expected", [
 5    ("True", True),
 6    ("TRUE", True),
 7    ("tRuE", True),
 8    ("false", False),
 9    ("FALSE", False),
10])
11def test_is_production(env_var, expected, monkeypatch):
12    monkeypatch.setenv("IS_PRODUCTION", env_var)
13    from const import is_production
14    assert is_production is expected
15    del sys.modules["const"]

This should ensure your tests all pass no matter what order they run in.