"Mocking where it's defined"在蟒蛇模拟?



我想测试一个不断发展的SQLite数据库应用程序,该应用程序并行使用"富有成效"。事实上,我正在通过将大型文本文件导入数据库并摆弄它来调查它们。我习惯于开发测试驱动,我不想在这次调查中放弃它。但是针对"生产"数据库运行测试感觉有些奇怪。因此,我的目标是针对测试数据库(真正的SQLite数据库,而不是模拟数据库)运行测试,该数据库包含受控但大量真实数据,显示了我在调查期间遇到的各种可变性。

为了支持这种方法,我有一个中央模块myconst.py包含一个返回数据库名称的函数,如下所示:

import myconst
conn = sqlite3.connect(myconst.get_db_path())

现在在unittest TestCase,我想过这样嘲笑:

@patch("myconst.get_db_name", return_value="../test.sqlite")
def test_this_and_that(self, mo):
    ...

其中测试调用的函数将在嵌套函数中使用 myconst.get_db_path() 访问数据库。

我试图先为自己做一些模拟,但它往往很笨拙且容易出错,所以我决定深入研究 python mock模块,如前所示。

不幸的是,我

到处都发现了警告,我应该"模拟它被使用的地方而不是它被定义的地方",就像这样:

@patch("mymodule.myconst.get_db_name", return_value="../test.sqlite")
def test_this_and_that(self, mo):
    self.assertEqual(mymodule.func_to_be_tested(), 1)

但是mymodule可能不会调用数据库函数本身,而是将其委托给另一个模块。这反过来意味着我的单元测试必须知道数据库实际访问的调用树——这是我真正想要避免的,因为它会导致重构代码时不必要的测试重构。

所以我试图创建一个最小的例子来理解mock的行为,以及它不允许我"在源头"嘲笑的地方。因为多模块设置在这里很笨拙,为了大家的方便,我也在 github 上提供了原始代码。看到这个:

myconst.py
----------
# global definition of the database name
def get_db_name():
    return "../production.sqlite"
# this will replace get_db_name()
TEST_VALUE = "../test.sqlite"
def fun():
    return TEST_VALUE
inner.py
--------
import myconst
def functio():
    return myconst.get_db_name()
print "inner:", functio()
test_inner.py
-------------
from mock import patch
import unittest
import myconst, inner
class Tests(unittest.TestCase):
    @patch("inner.myconst.get_db_name", side_effect=myconst.fun)
    def test_inner(self, mo):
        """mocking where used"""
        self.assertEqual(inner.functio(), myconst.TEST_VALUE)
        self.assertTrue(mo.called)
outer.py
--------
import inner
def functio():
    return inner.functio()
print "outer:", functio()
test_outer.py
-------------
from mock import patch
import unittest
import myconst, outer
class Tests(unittest.TestCase):
    @patch("myconst.get_db_name", side_effect=myconst.fun)
    def test_outer(self, mo):
        """mocking where it comes from"""
        self.assertEqual(outer.functio(), myconst.TEST_VALUE)
        self.assertTrue(mo.called)
unittests.py
------------
"""Deeply mocking a database name..."""
import unittest
print(__doc__)
suite = unittest.TestLoader().discover('.', pattern='test_*.py')
unittest.TextTestRunner(verbosity=2).run(suite)

test_inner.py工作就像上面链接的消息来源所说的那样,所以我期待它通过。 当我正确理解警告时,test_outer.py应该失败。但是所有的测试都毫无怨言地通过了!所以我的模拟一直在绘制,即使模拟函数像test_outer.py一样从调用堆栈中调用。从这个例子中,我可以得出结论,我的方法很安全,但另一方面,警告在相当多的来源中是一致的,我不想通过使用我不了解的概念来鲁莽地冒我的"生产"数据库的风险。

所以我的问题是:我是否误解了警告,或者这些警告只是过于谨慎?

最后我整理好了。也许这会对未来的访客有所帮助,所以我将分享我的发现:

像这样更改代码时:

inner.py
--------
from myconst import get_db_name
def functio():
    return get_db_name()
test_inner.py
-------------
@patch("inner.get_db_name", side_effect=myconst.fun)
def test_inner(self, mo):
    self.assertEqual(inner.functio(), myconst.TEST_VALUE)

test_inner会成功,但test_outer会打破

AssertionError: '../production.sqlite' != '../test.sqlite'

这是因为mock.patch不会替换引用的对象,在这两种情况下,该对象都是模块myconst中的函数get_db_namemock将替换作为第二个参数传递给测试的Mock对象"myconst.get_db_name"的名称的用法。

test_outer.py
-------------
@patch("myconst.get_db_name", side_effect=myconst.fun)
def test_outer(self, mo):
    self.assertEqual(outer.functio(), myconst.TEST_VALUE)

由于我在这里只模拟"myconst.getdb_name"并且inner.py通过"inner.get_db_name"访问get_db_name,因此测试将失败。

但是,通过使用正确的名称,可以解决此问题:

@patch("outer.inner.get_db_name", return_value=myconst.TEST_VALUE)
def test_outer(self, mo):
    self.assertEqual(outer.functio(), myconst.TEST_VALUE)

所以结论是,当我确保访问数据库的所有模块都包含myconst并使用myconst.get_db_name,我的方法将是安全的。或者,所有模块都可以from myconst import get_db_name并使用get_db_name。但我必须在全球范围内做出这个决定。

因为我控制所有代码访问get_db_name所以我是安全的。人们可以争论这是否是好的风格(假设是后者),但从技术上讲,它是安全的。我会嘲笑一个库函数吗,我几乎无法控制对该函数的访问,因此嘲笑"定义它的位置"是有风险的。这就是为什么引用的消息来源是警告。

最新更新