Search code examples
pythonflaskpytestmonkeypatching

How to correctly import object in fixture after changing env variables


I'm writing tests for the config file of a Flask application. To make sure the env variables set in the system do not influence the results of the test I'm using pytest's monkeypatch to create predictable test outcomes.

I'm testing the config file once in a 'clean' state with a fixture with no env variables set and once in a 'fake' config, with a fixture where I let monkeypatch set the variables before running the test.

Both fixtures set env variables and then import the config object before passing it on to the test function.

When the config object is loaded at the head of the document instead of inside the fixtures, both fixtures use a version based on the actual system env variables.

It seems like the second fixture does not import the config object, but reuses the one created by the cleanConfig fixture. How can I force the fixture to reimport the config object?

test_config.py:

import pytest
from config import config

class TestConfigSettings(object):

@pytest.fixture(scope='function')
def cleanConfig(config_name, monkeypatch):
    def makeCleanConfig(config_name):

        monkeypatch.delenv('SECRET_KEY', raising=False)
        monkeypatch.delenv('DEV_DATABASE_URL', raising=False)

        from config import config
        configObject = config[config_name]

        return configObject
    return makeCleanConfig


@pytest.fixture(scope='function')
def fakeEnvConfig(config_name, monkeypatch):
    def makeFakeEnvConfig(config_name):

        monkeypatch.setenv('SECRET_KEY', 'fake difficult string')
        monkeypatch.setenv('DEV_DATABASE_URL', 'postgresql://fake:5432/fakeDevUrl')

        from config import config
        configObject = config[config_name]

        return configObject
    return makeFakeEnvConfig


def test_configObject_withDevelopmentConfig_containsCorrectSettings(self, cleanConfig):
    configObject = cleanConfig('development')

    assert configObject.SECRET_KEY == 'hard to guess string'
    assert configObject.DEBUG == True
    assert configObject.SQLALCHEMY_DATABASE_URI == None

def test_configObject_withDevelopmentConfigAndEnvSet_copiesEnvSettings(self, fakeEnvConfig):
    configObject = fakeEnvConfig('development')

    assert configObject.SECRET_KEY == 'fake difficult string'
    assert configObject.SQLALCHEMY_DATABASE_URI == 'postgresql://fake:5432/fakeDevUrl'

Config.py:

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL')

config = {
    'default': DevelopmentConfig,
    'development': DevelopmentConfig,
    ...
}

Solution

  • I finally found a solution for my problem. By using the reload() function, you can import a module again after changing the content (the loaded env variables in this case). To be able to use it I had to change the import to the config module instead of the config dictionary I was importing before, since the reload() object only works on modules. The new code:

    import pytest
    from importlib import reload
    import config
    
    class TestConfigSettings(object):
    
    @pytest.fixture(scope='function')
    def cleanConfig(config_name, monkeypatch):
        def makeCleanConfig(config_name):
    
            monkeypatch.delenv('SECRET_KEY', raising=False)
            monkeypatch.delenv('DEV_DATABASE_URL', raising=False)
    
            reload(config)
            configObject = config.config[config_name]
    
            return configObject
        return makeCleanConfig
    
    @pytest.fixture(scope='function')
    def fakeEnvConfig(config_name, monkeypatch):
        def makeFakeEnvConfig(config_name):
    
            monkeypatch.setenv('SECRET_KEY', 'fake difficult string')
            monkeypatch.setenv('DEV_DATABASE_URL', 'postgresql://fake:5432/fakeDevUrl')
    
            reload(config)
            configObject = config.config[config_name]
    
            return configObject
        return makeFakeEnvConfig