You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

原因分析

  1. 模块加载顺序冲突:database.py在Pytest测试发现阶段就被导入,此时LOCAL_SESSION会立即执行创建逻辑,而conftest的fixture要到测试运行阶段才会生效。Mock还没启动,真实的配置就已经被加载并创建了数据库连接。
  2. 单例初始化时机问题:Config单例在database.py导入时就完成了初始化,内部的__table_db_path私有属性已经被赋值为原始配置值。直接patch类的table_db_path property,无法覆盖已经初始化完成的单例实例的私有属性。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 23:24:56