Search code examples
pythonpytestfixtures

Pytest environment fixtures not seen by imported class


I'm trying to set up pytest so that whenever I run my tests (locally or in github actions), the environment variables all point to files and locations in my test directory instead of wherever they're set to based on the user.

The problem is, the fixture changes are visible if I add an ipdb trace in the test_database function and print os.getenv('DB_URL') but the assert will always fail because the DataBase object always has the original non-mocked url (set in .bash_profile).

database.py

import h5py
import os

class DataBase:

    route = os.environ.get('DB_URL')

    def __init__(self):
        self.connected = False

    def connect(self):
        if not connected:
            self.db = h5py.File(self.route, 'r')
            self.connected = True

conftest.py

import os
import pytest

@pytest.fixture(autouse=True)
def mock_test_env(monkeypatch):
    cwd = os.getcwd()
    monkeypatch.setenv('DB_URL', cwd + '/sample_db.hdf5')

test_database.py

import pytest
from repo import DataBase

def test_database():
    db = DataBase()
    import ipdb; ipdb.set_trace()
    '''
    os.getenv('DB_URL') returns cwd + '/sample_db.hdf5'
    db.route returns original database, not the sample one above
    '''
    assert db.connected = False, 'DataBase must be instantiated to connected == False'

How do I globally set environment variables so all objects see the same envs?


Solution

  • As others have mentioned in your comment that class variables to be avoided for this assignment because it's a constant which gets assigned the moment the import statement is scanned.

    To better understand this situation, try placing the from repo import DataBase inside your method

    def test_database():

    Like this:

    import os
    import pytest
    
    @pytest.fixture(autouse=True)
    def mock_test_env(monkeypatch):
        cwd = os.getcwd()
        monkeypatch.setenv('DB_URL', cwd + '/sample_db.hdf5')
    
    def test_database(mock_test_env):
        from repo import DataBase # <<< This here works
        db = DataBase()
        assert db.route == (os.getcwd() + '/sample_db.hdf5') # Works!
    

    Now, when you place the from repo import Database at the start of the file, pytest would scan and start reading all the imports and starts initialising the blueprints and sets the value of router the moment its imported.

    Read: When is the class variable initialised in Python?

    So the ideal way would be to avoid maybe such important initialisation and assing the same in the constructor of the Database class. Thus ensuring it does calculate when needed.

    I feel, I for one like to think of it like Explicit is better than implicit. from the Zen Of Python and do it like:

    import h5py
    import os
    
    class DataBase:
    
        def __init__(self):
            self.route = os.environ.get('DB_URL')
            self.connected = False
    
        def connect(self):
            if not connected:
                self.db = h5py.File(self.route, 'r')
                self.connected = True