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

8
在我的应用程序中,每个模型都有一个类,保存常用查询(我猜这在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()调用时遇到了困难。目前我在单元测试中使用monkey patching来替换Post.date.desc的mock,但是我觉得可能有更好的方法。

编辑:我正在使用mox进行mock对象,我的当前单元测试大致如下:

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')" 这部分代码就会失败。

2个回答

13

我认为使用mock对象来测试查询并没有太大的益处。测试应该测试代码的逻辑,而不是实现。更好的解决方案是创建一个新的数据库,在其中添加一些对象,运行查询,并确定是否获得了正确的结果。例如:


# 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可以为您提供快速测试,但如果您想认真对待这些测试,您可能需要对与生产中使用的相同类型的数据库运行它们。


这很有道理,我太过于纠结于代码的细节了。感谢您的洞察力。 - rr.
8
你所说的是,与其进行单元测试(测试逻辑单元),不如进行与数据库的集成测试。这是一种有效的方法,也可能是ORM中最合理的方法,但这些测试的性能会下降一个或两个数量级。对吗? - Peter Lada
1
你说得对,编写一个测试(无论你想称其为“单元”测试还是“集成”测试),它将以某种方式访问较慢的资源(无论是磁盘驱动器、外部服务还是数据库),这将意味着比不访问这样的资源的测试要慢。 - Mark Hildreth
2
@MarkHildreth你可能会称它为集成测试,这很好。通过不模拟解决模拟问题有时是恰当的,但不把它称为本质也有些奇怪。 - Rob Grant

1
如果您想使用模拟输入来创建单元测试,可以使用虚假数据创建模型的实例。
如果结果代理返回来自多个模型的数据(例如当您连接两个表时),您可以使用名为namedtuplecollections数据结构。
我们正在使用它来模拟连接查询的结果。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接