简体   繁体   English

在python模拟中“嘲笑它的定义”?

[英]"Mocking where it's defined" in python mock?

I want to test an evolving SQLite database application, which is in parallel used "productively".我想测试一个不断发展的 SQLite 数据库应用程序,该应用程序并行使用“富有成效”。 In fact I am investigating a pile of large text files by importing them to the database and fiddling around with it.事实上,我正在通过将它们导入数据库并摆弄它来调查一堆大型文本文件。 I am used to develop test-driven, and I do not want to drop that for this investigation.我习惯于开发测试驱动,我不想在这次调查中放弃它。 But running tests against the "production" database feels somewhat strange.但是对“生产”数据库运行测试感觉有些奇怪。 So my objective is to run the tests against a test database (a real SQLite database, not a mock) containing a controlled, but considerable amount of real data showing all kinds of variability I have met during the investigation.因此,我的目标是针对包含受控但大量真实数据的测试数据库(一个真正的 SQLite 数据库,而不是模拟数据库)运行测试,这些数据显示了我在调查期间遇到的各种可变性。

To support this approach, I have a central module myconst.py containing a function returning the name of the database that is used like so:为了支持这种方法,我有一个中央模块myconst.py其中包含一个返回数据库名称的函数,使用方式如下:

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

Now in the unittest TestCase s, I thought about mocking like so:现在在unittest TestCase ,我想像这样模拟:

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

where the test calls functions that will, in nested functions, access the database using myconst.get_db_path() .测试调用的函数将在嵌套函数中使用myconst.get_db_path()访问数据库。

I have tried to do a little mocking for myself first, but it tends to be clumsy and error prone so I decided to dive into the python mock module as shown before.我曾尝试先对自己进行一些mock ,但它往往很笨拙且容易出错,因此我决定深入研究之前所示的 python mock模块。

Unfortunately, I found warnings all over , that I am supposed to "mock where it's used and not where it's defined" like so:不幸的是,我发现警告所有“使用它的模拟,而不是在那里的定义”,我应该像这样:

@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)

But mymodule will likely not call database functions itself but delegate that to another module.但是mymodule可能不会自己调用数据库函数,而是将其委托给另一个模块。 This in turn would imply that my unit tests have to know the call tree where the database is actually access – something I really want to avoid because it would lead to unnecessary test refactoring when the code is refactored.这反过来意味着我的单元测试必须知道实际访问数据库的调用树——我真的想避免这一点,因为在重构代码时会导致不必要的测试重构。

So I tried to create a minimal example to understand the behavior of mock and where it fails to allow me to mock "at the source".所以我试图创建一个最小的例子来理解mock的行为以及它无法让我“在源头”模拟的地方。 Because a multi module setup is clumsy here, I have provided the original code also on github for everybody's convenience.因为这里的多模块设置很笨拙,为了方便大家,我也在github上提供了原始代码。 See this:看到这个:

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 works like the sources linked above say, and so I was expecting it to pass. test_inner.py就像上面链接的来源一样工作,所以我期待它通过。 test_outer.py should fail when I understand the caveats right.当我正确理解警告时, test_outer.py应该会失败。 But all the tests pass without complaint!但是所有的测试都毫无怨言地通过了! So my mock is drawn all the time, even when the mocked function is called from down the callstack like in test_outer.py .所以我的模拟一直被绘制,即使模拟函数是从调用堆栈中调用的,就像在test_outer.py From that example I would conclude that my approach is safe, but on the other hand the warnings are consistent across quite some sources and I do not want to recklessly risk my "production" database by using concepts that I do not grok .从这个例子中,我可以得出结论,我的方法是安全的,但在另一方面,警告是横跨相当长的一段源一致的,我不希望使用的概念,我不硬拼冒险我“生产”数据库神交

So my question is: Do I misunderstand the warnings or are these warnings just over-cautious?所以我的问题是:我是误解了警告还是这些警告过于谨慎?

Finally I sorted it out.最后我整理了一下。 Maybe this will help future visitors, so I will share my findings:也许这会对未来的访问者有所帮助,所以我将分享我的发现:

When changing the code like so :像这样更改代码时:

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 will succeed, but test_outer will break with test_inner会成功,但test_outer会中断

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

This is because mock.patch will not replace the referenced object , which is function get_db_name in module myconst in both cases.这是因为mock.patch不会替换引用的object ,在这两种情况下都是模块myconst中的函数get_db_name mock will instead replace the usages of the name "myconst.get_db_name" by the Mock object passed as the second parameter to the test. 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)

Since I mock only "myconst.getdb_name" here and inner.py accesses get_db_name via "inner.get_db_name" , the test will fail.因为我只是嘲笑"myconst.getdb_name"这里inner.py访问get_db_name通过"inner.get_db_name" ,会导致测试失败。

By using the proper name, however, this can be fixed:但是,通过使用正确的名称,可以解决此问题:

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

So the conclusion is that my approach will be safe when I make sure that all modules accessing the database include myconst and use myconst.get_db_name .所以结论是,我确保访问数据库的所有模块都包含myconst并使用myconst.get_db_name,我的方法将是安全的。 Alternatively all modules could from myconst import get_db_name and use get_db_name .或者,所有模块都可以from myconst import get_db_name并使用get_db_name But I have to draw this decision globally.但我必须在全球范围内做出这个决定。

Because I control all code accessing get_db_name I am safe.因为我控制所有访问get_db_name代码我是安全的。 One can argue whether this is good style or not (assumingly the latter), but technically it's safe.人们可以争论这是否是好的风格(假设是后者),但从技术上讲它是安全的。 Would I mock a library function instead, I could hardly control access to that function and so mocking "where it's defined" becomes risky.我会模拟一个库函数吗,我几乎无法控制对该函数的访问,因此模拟“它的定义位置”变得有风险。 This is why the sources cited are warning.这就是为什么引用的消息来源是警告的原因。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM