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.

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

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s