【问题标题】:Python SQLAlchemy - Mocking a model attribute's "desc" methodPython SQLAlchemy - 模拟模型属性的“desc”方法
【发布时间】:2011-07-04 13:02:22
【问题描述】:

在我的应用程序中,每个模型都有一个包含常用查询的类(我猜它有点像 DDD 语言中的“存储库”)。这些类中的每一个都被传递给 SQLAlchemy 会话对象,以在构造时创建查询。我很难确定在我的单元测试中运行某些查询的最佳方式。使用无处不在的博客示例,假设我有一个“发布”模型,其中包含列和属性“日期”和“内容”。我还有一个带有“find_latest”方法的“PostRepository”,它应该按“日期”降序查询所有帖子。它看起来像:

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

这确实有效,但感觉很难看,我不确定为什么没有“Post.date.desc().AndReturn('test')”中的“AndReturn('test')”片段会失败

【问题讨论】:

    标签: python unit-testing mocking sqlalchemy mox


    【解决方案1】:

    如果您还想使用模拟输入创建单元测试,您可以使用假数据创建模型实例

    如果结果代理返回的结果包含来自多个模型的数据(例如,当您连接两个表时),您可以使用名为 namedtuplecollections 数据结构

    我们正在使用它来模拟连接查询的结果

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

      • 这很有意义,我对代码的细节过于关注了。感谢您的洞察力。
      • 您的意思是,不要进行单元测试(逻辑单元),而是进行集成测试(使用数据库)。这是一种有效的方法,对于 ORM 来说可能是最明智的,但这些测试的性能会受到一两个量级的影响。对吗?
      • 您是正确的,编写测试(无论您希望将其称为“单元”测试还是“集成”测试)会以某种方式访问​​较慢的资源(无论是磁盘驱动器、外部服务或数据库)将意味着比不访问此类资源的测试要慢。
      • @MarkHildreth 您可能会称其为集成测试,这很好。通过不模拟来解决模拟问题有时是合适的,但不这么称呼它似乎很奇怪。
      猜你喜欢
      • 2014-06-03
      • 2019-06-13
      • 1970-01-01
      • 2013-05-27
      • 2018-11-10
      • 1970-01-01
      • 2020-07-07
      • 1970-01-01
      相关资源
      最近更新 更多