如何组织数据库访问层?

时间:2009-08-25 05:35:09

标签: python database testing orm mocking

我正在使用SqlAlchemy,一个python ORM库。我曾经通过调用SqlAlchemy API直接从业务层访问数据库。

但后来我发现这会导致我运行所有测试用例的时间过长,现在我想也许我应该创建一个数据库访问层,所以我可以在测试期间使用模拟对象而不是直接访问数据库。

我认为有两种选择:

  1. 使用包含数据库连接的单个类以及addUser / delUser / updateUser,addBook / delBook / updateBook等许多方法。但这意味着会非常大。

  2. 另一种方法是创建不同的管理器类,如“UserManager”,“BookManager”。但这意味着我必须将经理列表传递给业务层,这看起来有点麻烦。

  3. 您将如何组织数据库层?

4 个答案:

答案 0 :(得分:5)

这是一个很好的问题!
问题不是微不足道的,可能需要几种方法来解决它。 例如:

  1. 组织代码,以便您可以在不访问数据库的情况下测试大多数应用程序逻辑。这意味着每个类都有访问数据的方法和处理数据的方法,第二个类可以很容易地进行测试。
  2. 当您需要测试数据库访问权限时,您可以使用代理(因此,如解决方案#1);您可以将其视为SqlAlchemy的引擎或SA的替代品。在这两种情况下,您可能需要考虑self initializing fake
  3. 如果代码不涉及存储过程,请考虑使用内存数据库,如Lennart所说(即使在这种情况下,将其称为“单元测试”可能听起来有点奇怪!)。
  4. 但是,从我的经验来看,一切都很容易,然后当你去场时突然下降。例如,当大多数逻辑在SQL语句中时该怎么办?如果访问数据与其处理严格交错怎么办?有时你可能有时(特别是对于大型和遗留应用程序)重构。

    最后,我认为这主要是心态的问题 如果您认为需要进行单元测试,并且需要让它们快速运行,那么您可以通过某种方式设计应用程序,以便更轻松地进行单元测试。
    不幸的是,这并不总是正确的(许多人认为单元测试可以在一夜之间运行,因此时间不是问题),并且你得到的东西不是真正的单元可测试的。

答案 1 :(得分:2)

我会在测试期间建立一个连接到内存数据库的数据库连接。像这样:

sqlite_memory_db = create_engine('sqlite://')

这将是你可以获得的速度,你也没有连接到真正的数据库,而只是内存中的临时数据库,所以你不必担心你的测试后所做的更改测试,等等。你不必嘲笑任何东西。

答案 2 :(得分:2)

捕获对数据库的修改的一种方法是使用SQLAlchemy会话扩展机制并使用以下内容拦截对数据库的刷新:

from sqlalchemy.orm.attributes import instance_state
from sqlalchemy.orm import SessionExtension

class MockExtension(SessionExtension):
    def __init__(self):
        self.clear()

    def clear(self):
        self.updates = set()
        self.inserts = set()
        self.deletes = set()

    def before_flush(self, session, flush_context, instances):
        for obj in session.dirty:
            self.updates.add(obj)
            state = instance_state(obj)
            state.commit_all({})
            session.identity_map._mutable_attrs.discard(state)
            session.identity_map._modified.discard(state)

        for obj in session.deleted:
            self.deletes.add(obj)
            session.expunge(obj)

        self.inserts.update(session.new)
        session._new = {}

然后,对于测试,您可以使用该模拟配置会话,并查看它是否符合您的期望。

mock = MockExtension()
Session = sessionmaker(extension=[mock], expire_on_commit=False)

def do_something(attr):
    session = Session()
    obj = session.query(Cls).first()
    obj.attr = attr
    session.commit()

def test_something():
    mock.clear()
    do_something('foobar')
    assert len(mock.updates) == 1
    updated_obj = mock.updates.pop()
    assert updated_obj.attr == 'foobar'

但是你至少要对数据库进行一些测试,因为你至少想知道你的查询是否按预期工作。请注意,您还可以通过session.update().delete().execute()对数据库进行修改。

答案 3 :(得分:0)

SQLAlchemy为making mocking easier提供了一些功能 - 也许这比尝试重写项目的整个部分更容易?