Home

Oct. 7, 2016, 3 min read

Monkeypatching Module Globals

This is an issue I came across when trying to write a test to ensure a configuration is correctly read from a number of environment variables: It can be tricky when module global variables are part of the setup. I am using pytest here, but the problem would happen with any method that patches the environment. Let's say I have a module

conf.py

like this:

import os

SETTING_A = int(os.getenv("SETTING_A", "1"))
SETTING_B = int(os.getenv("SETTING_B", "1"))

Say we want to write two tests in

test_conf.py

to check that the value is parsed correctly from the environment, one test with the default and one with a monkeypatched environment:

import pytest
import conf

@pytest.fixture
def patched_env(monkeypatch):
    monkeypatch.setenv("SETTING_A", "0")
    monkeypatch.setenv("SETTING_B", "0")

def test_conf_with_patched_env(patched_env):
    assert conf.SETTING_A == 0
    assert conf.SETTING_B == 0

def test_conf_with_default_env():
    assert conf.SETTING_A == 1
    assert conf.SETTING_B == 1

What will happen? The

test_conf_with_patched_env()

will fail, since the values inside

conf.py

are already set on import time and not changed by the

monkeypatch.setenv()

calls. No problem, we can just reload the module after monkeypatching to refresh those values, can't we?

@pytest.fixture
def patched_env(monkeypatch):
    monkeypatch.setenv("SETTING_A", "0")
    monkeypatch.setenv("SETTING_B", "0")
    reload(conf)

Now

test_conf_with_patched_env()

will pass, the test using the defaults however will fail

(note that this depends on the test order, so it might have worked when swapping the test function definitions, but the problem still exists). The monkeypatch changes are kept for all following tests, since again the global module state has been changed and is not reset for every test. We could add a

reload(conf)

in every test that needs it, or write a fixture for it. In my case I worked around this problem with a function scoped autouse fixture inside the directory's

conftest.py

@pytest.fixture(autouse=True)
def reload_conf_module():
    reload(conf)

This ensures a clean global state before any test is run, so that all tests pass. Yes alright, it is still a hack. A better approach would be to avoid using global module variables in this case or not storing the values at all and instead use getter functions.