Pytest测试发现触发sqlite3.OperationalError:无法打开数据库文件
问题:Pytest中Mock单例Config属性失败,导致数据库连接错误
背景代码
config.py(单例配置类)
# config.py from yaml import load, SafeLoader from loguru import logger """ Populate custom configuration for the SERVICE component """ APPNAME="acceloop-search-engine" @logger.catch def _get_field(config, fieldname): """Get specified field from yaml config file Args: config (dict): dictionary containing the parsed config yaml fieldname (str): the name of the field to be searched Returns: str: the value corresponding to the given field name """ if fieldname not in config: raise SystemExit(f"No field named {fieldname} is present in yaml configuration file, please add it.") if not config[fieldname]: raise SystemExit(f"Field {fieldname} is present in yaml configuration file, but must not be empty string, null or None. Please correctly evaluate it.") return config[fieldname] @logger.catch def _get_loader(): """Get customizable yaml loader Returns: loader: customized yaml loader """ loader = SafeLoader return loader @logger.catch def _get_config(config_filename): """Get dictionary containing the parsed yaml config file Returns: dict: dictionary containing the parsed config yaml """ return load(open(config_filename, "rb"), Loader=_get_loader()) class Config(object): def __new__(cls): if not hasattr(cls, "instance"): cls.instance = super(Config, cls).__new__(cls) cls.instance._init_singleton() return cls.instance @logger.catch def _init_singleton(self): try: __config = _get_config("config.yml") except Exception as error: raise SystemExit(f"Error while loading yaml config file: {error}") __app_config = _get_field(__config, "app") self.__table_db_path = _get_field(__app_config, "table-db-path") @property def table_db_path(self): return self.__table_db_path
database.py(数据库连接模块)
# database.py import sqlite3 from urllib.request import pathname2url from .config import Config try: dburi = "file:{}?mode=r".format(pathname2url(Config().table_db_path)) LOCAL_SESSION = sqlite3.connect(dburi, uri=True) except sqlite3.OperationalError: raise SystemExit(f"No db {Config().table_db_path} found. Please create it using the correct service.")
test_database.py(测试文件)
# test_database.py from src.database import LOCAL_SESSION def test_should_return_true_when_read_from_db_is_given(): with LOCAL_SESSION.cursor() as cursor: cursor.execute(""" SELECT count(*) AS tot FROM table """) assert cursor.fetchone()[0] == 1
conftest.py(Mock配置)
# conftest.py import pytest from unittest.mock import patch from src.config import Config @pytest.fixture(scope="session", autouse=True) def mock_config_table_db_path(): with patch.object(Config, "table_db_path", "./tests/resources/ase_service.db"): yield print("CONFTEST Loaded")
问题现象
测试运行时触发以下错误,且Mock的table_db_path未生效,依然是原始配置值:
sqlite3.OperationalError: unable to open database file
原因分析
- 模块加载顺序冲突:
database.py在Pytest测试发现阶段就被导入,此时LOCAL_SESSION会立即执行创建逻辑,而conftest的fixture要到测试运行阶段才会生效。Mock还没启动,真实的配置就已经被加载并创建了数据库连接。 - 单例初始化时机问题:Config单例在
database.py导入时就完成了初始化,内部的__table_db_path私有属性已经被赋值为原始配置值。直接patch类的table_db_pathproperty,无法覆盖已经初始化完成的单例实例的私有属性。 - Patch对象错误:
table_db_path是实例的@property,你patch的是类的属性,但单例实例已经持有自己的私有属性值,类级别的patch不会影响已存在的实例。
解决方案
方案1:延迟数据库连接初始化
修改database.py,将LOCAL_SESSION的创建改为延迟执行,确保Mock生效后再初始化连接:
# database.py import sqlite3 from urllib.request import pathname2url from .config import Config def get_local_session(): try: dburi = "file:{}?mode=r".format(pathname2url(Config().table_db_path)) return sqlite3.connect(dburi, uri=True) except sqlite3.OperationalError: raise SystemExit(f"No db {Config().table_db_path} found. Please create it using the correct service.")
对应的测试文件修改:
# test_database.py from src.database import get_local_session def test_should_return_true_when_read_from_db_is_given(): with get_local_session().cursor() as cursor: cursor.execute(""" SELECT count(*) AS tot FROM table """) assert cursor.fetchone()[0] == 1
方案2:直接Mock单例实例的私有属性
针对已初始化的单例实例,直接修改其私有属性,并重新创建数据库连接:
# conftest.py import pytest from src.config import Config from src import database import sqlite3 from urllib.request import pathname2url @pytest.fixture(scope="session", autouse=True) def mock_config_table_db_path(): # 获取单例实例 config = Config() # 访问私有属性(Python名称改写规则:_类名__属性名) config._Config__table_db_path = "./tests/resources/ase_service.db" # 重新初始化数据库连接 dburi = "file:{}?mode=r".format(pathname2url(config.table_db_path)) database.LOCAL_SESSION = sqlite3.connect(dburi, uri=True) yield
方案3:给Config类添加测试友好的方法
修改Config类,增加重置和属性设置方法,方便测试时修改配置:
# config.py class Config(object): # ... 原有代码 ... def reset_singleton(self): """重置单例,用于测试场景""" if hasattr(self.__class__, "instance"): delattr(self.__class__, "instance") def set_table_db_path(self, path): """手动设置数据库路径,用于测试""" self.__table_db_path = path
对应的conftest修改:
# conftest.py import pytest from src.config import Config from src import database import sqlite3 from urllib.request import pathname2url @pytest.fixture(scope="session", autouse=True) def mock_config_table_db_path(): # 重置单例,避免加载原始配置 Config().reset_singleton() # 创建新的单例并设置测试路径 config = Config() config.set_table_db_path("./tests/resources/ase_service.db") # 重新初始化数据库连接 dburi = "file:{}?mode=r".format(pathname2url(config.table_db_path)) database.LOCAL_SESSION = sqlite3.connect(dburi, uri=True) yield
内容的提问来源于stack exchange,提问作者Chairos
相关产品推荐
相关产品推荐

