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:

is_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:

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

import pytest

from const import is_production

def test_is_production_is_false(monkeypatch):
    monkeypatch.setenv("IS_PRODUCTION", "false")
    assert is_production is False
    # True because the import and therefore the os.getenv(...) default value
    # 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.

import pytest

def test_is_production_is_false(monkeypatch):
    monkeypatch.setenv("IS_PRODUCTION", "false")
    from const import is_production
    # Now monkeypatch patches the environment variable before the call to
    # os.getenv(...) is executed
    assert is_production is False

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

import pytest

# Note the quotation marks
@pytest.mark.parametrize("env_var, expected", [
    ("True", True),
    ("TRUE", True),
    ("tRuE", True),
    ("false", False),
    ("FALSE", False),
])
def test_is_production(env_var, expected, monkeypatch):
    monkeypatch.setenv("IS_PRODUCTION", env_var)
    from const import is_production
    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.

import sys
import pytest

@pytest.mark.parametrize("env_var, expected", [
    ("True", True),
    ("TRUE", True),
    ("tRuE", True),
    ("false", False),
    ("FALSE", False),
])
def test_is_production(env_var, expected, monkeypatch):
    monkeypatch.setenv("IS_PRODUCTION", env_var)
    from const import is_production
    assert is_production is expected
    del sys.modules["const"]

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