Python SQLAlchemy - 模拟模型属性的 "desc" 方法

8 投票
2 回答
14131 浏览
提问于 2025-04-16 12:33

在我的应用程序中,每个模型都有一个类,用来存放常用的查询(我想这在领域驱动设计(DDD)中算是一个“仓库”)。这些类在创建时会接收一个 SQLAlchemy 的会话对象,以便用来生成查询。我现在有点困惑,不知道在单元测试中怎么最好地验证某些查询确实被执行了。用一个常见的博客例子来说明,假设我有一个“Post”模型,它有“date”和“content”这两个字段。我还有一个“PostRepository”,里面有一个叫“find_latest”的方法,应该是用来按“date”降序查询所有帖子。大概是这样的:

from myapp.models import Post

class PostRepository(object):
    def __init__(self, session):
        self._s = session

    def find_latest(self):
        return self._s.query(Post).order_by(Post.date.desc())

我在模拟 Post.date.desc() 这个调用时遇到了麻烦。目前我在单元测试中用猴子补丁的方式替换了 Post.date.desc,但我觉得可能有更好的方法。

补充说明:我在使用 mox 来处理模拟对象,我现在的单元测试大概是这样的:

import unittest
import mox

class TestPostRepository(unittest.TestCase):

    def setUp(self):
        self._mox = mox.Mox()

    def _create_session_mock(self):
        from sqlalchemy.orm.session import Session
        return self._mox.CreateMock(Session)

    def _create_query_mock(self):
        from sqlalchemy.orm.query import Query
        return self._mox.CreateMock(Query)

    def _create_desc_mock(self):
        from myapp.models import Post
        return self._mox.CreateMock(Post.date.desc)

    def test_find_latest(self):
        from myapp.models.repositories import PostRepository
        from myapp.models import Post

        expected_result = 'test'

        session_mock = self._create_session_mock()
        query_mock = self._create_query_mock()
        desc_mock = self._create_desc_mock()

        # Monkey patch
        tmp = Post.date.desc
        Post.date.desc = desc_mock

        session_mock.query(Post).AndReturn(query_mock)
        query_mock.order_by(Post.date.desc().AndReturn('test')).AndReturn(query_mock)
        query_mock.offset(0).AndReturn(query_mock)
        query_mock.limit(10).AndReturn(expected_result)

        self._mox.ReplayAll()
        r = PostRepository(session_mock)

        result = r.find_latest()
        self._mox.VerifyAll()

        self.assertEquals(expected_result, result)

        Post.date.desc = tmp

这个方法是有效的,但感觉不太好,我也不明白为什么没有“AndReturn('test')”这一部分的“Post.date.desc().AndReturn('test')”会失败。

2 个回答

1

如果你想用假数据来创建单元测试,可以用虚假的数据来实例化你的模型。

如果结果代理返回的数据来自多个模型(比如当你连接两个表的时候),你可以使用一种叫做 namedtuple 的数据结构,它属于 collections

我们用它来模拟连接查询的结果。

13

我觉得用模拟对象来测试你的查询其实没什么太大好处。测试应该是检查代码的逻辑,而不是实现。一个更好的方法是创建一个新的数据库,往里面添加一些数据,然后在这个数据库上运行查询,看看你得到的结果是否正确。例如:


# Create the engine. This starts a fresh database
engine = create_engine('sqlite://')
# Fills the database with the tables needed.
# If you use declarative, then the metadata for your tables can be found using Base.metadata
metadata.create_all(engine)
# Create a session to this database
session = sessionmaker(bind=engine)()

# Create some posts using the session and commit them
...

# Test your repository object...
repo = PostRepository(session)
results = repo.find_latest()

# Run your assertions of results
...

这样,你实际上是在测试代码的逻辑。这意味着你可以改变方法的实现方式,只要查询能正确工作,测试就应该通过。如果你愿意,可以把这个方法写成一个查询,获取所有对象,然后对结果列表进行切片。测试会通过,因为它应该是这样的。之后,你可以把实现改成使用SA表达式API来运行查询,测试依然会通过。

需要注意的一点是,sqlite的行为可能和其他类型的数据库不一样。使用sqlite的内存模式可以让测试变得很快,但如果你想认真对待这些测试,可能还是要在和生产环境中使用的同类型数据库上进行测试。

撰写回答