“嘲笑它定义的地方"在 python 模拟中? [英] "Mocking where it's defined" in python mock?

查看:22
本文介绍了“嘲笑它定义的地方"在 python 模拟中?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I want to test an evolving SQLite database application, which is in parallel used "productively". 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.

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

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:

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

现在在 unittest TestCases 中,我想像这样模拟:

Now in the unittest TestCases, I thought about mocking like so:

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

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

where the test calls functions that will, in nested functions, access the database using myconst.get_db_path().

我曾尝试先对自己进行一些嘲笑,但它往往笨拙且容易出错,所以我决定深入研究 python mock 模块,如前所示.

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.

不幸的是,我发现了警告 所有 超过,我应该模拟它的使用位置而不是它的定义位置",就像这样:

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)

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

But mymodule will likely not call database functions itself but delegate that to another module. 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.

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

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". Because a multi module setup is clumsy here, I have provided the original code also on github for everybody's convenience. 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 就像上面链接的来源一样工作,所以我期待它通过.test_outer.py 当我理解正确的警告时应该会失败.但是所有的测试都毫无怨言地通过了!所以我的模拟一直在绘制,即使模拟函数是从调用堆栈中调用的,就像在 test_outer.py 中一样.从那个例子中,我会得出结论,我的方法是安全的,但另一方面,警告在相当多的来源中是一致的,我不想通过使用我不使用的概念来鲁莽地冒险我的生产"数据库grok.

test_inner.py works like the sources linked above say, and so I was expecting it to pass. test_outer.py should fail when I understand the caveats right. 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. 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:

更改代码时像这样:

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 会中断

test_inner will succeed, but test_outer will break with

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

这是因为mock.patch 不会替换引用的对象,即myconstget_db_name> 在这两种情况下.mock 将代替 name "myconst.get_db_name" 的用法替换为作为第二个传递的 Mock 对象测试的参数.

This is because mock.patch will not replace the referenced object, which is function get_db_name in module myconst in both cases. 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.

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/code>,测试将失败.

Since I mock only "myconst.getdb_name" here and inner.py accesses get_db_name via "inner.get_db_name", the test will fail.

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

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)

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

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. Alternatively all modules could from myconst import get_db_name and use get_db_name. But I have to draw this decision globally.

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

Because I control all code accessing get_db_name I am safe. 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.

这篇关于“嘲笑它定义的地方"在 python 模拟中?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆