我想测试一个不断发展的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_name
。 mock
将替换作为第二个参数传递给测试的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
所以我是安全的。人们可以争论这是否是好的风格(假设是后者),但从技术上讲,它是安全的。我会嘲笑一个库函数吗,我几乎无法控制对该函数的访问,因此嘲笑"定义它的位置"是有风险的。这就是为什么引用的消息来源是警告。