如何正确使用模拟在 python 与单元测试设置

在我尝试学习 TDD 的过程中,我尝试学习单元测试和使用 python 模拟。慢慢地掌握了窍门,但不确定我是否做对了。预先警告: 我坚持使用 python 2.4,因为供应商的 API 是预编译的2.4 pyc 文件,所以我使用的是 mock 0.8.0和 unittest (而不是 unittest2)

给定‘ mymodule.py’中的示例代码

import ldap


class MyCustomException(Exception):
pass


class MyClass:
def __init__(self, server, user, passwd):
self.ldap = ldap.initialize(server)
self.user = user
self.passwd = passwd


def connect(self):
try:
self.ldap.simple_bind_s(self.user, self.passwd)
except ldap.INVALID_CREDENTIALS:
# do some stuff
raise MyCustomException

现在在我的测试用例文件‘ test _ myclass.py’中,我想模拟 ldap 对象。Initialize 返回 ldap.ldapobject。SimpleLDAPObject,所以我想这就是我要模拟出来的方法。

import unittest
from ldap import INVALID_CREDENTIALS
from mock import patch, MagicMock
from mymodule import MyClass


class LDAPConnTests(unittest.TestCase):
@patch('ldap.initialize')
def setUp(self, mock_obj):
self.ldapserver = MyClass('myserver','myuser','mypass')
self.mocked_inst = mock_obj.return_value


def testRaisesMyCustomException(self):
self.mocked_inst.simple_bind_s = MagicMock()
# set our side effect to the ldap exception to raise
self.mocked_inst.simple_bind_s.side_effect = INVALID_CREDENTIALS
self.assertRaises(mymodule.MyCustomException, self.ldapserver.connect)


def testMyNextTestCase(self):
# blah blah

让我想到几个问题:

  1. 看起来合适吗? :)
  2. 这是否是尝试和模拟在我正在测试的类中实例化的对象的正确方法?
  3. 在 setUp 上调用@patch 装饰器可以吗? 还是会产生奇怪的副作用?
  4. 是否有办法获取 mock 来引发 ldap.INVALID _ CREDENTIALS 异常,而不必将异常导入到我的 testcase 文件中?
  5. 我应该改用 patch.object ()吗? 如果应该,怎么做?

谢谢。

74987 次浏览

您可以将 patch()用作类装饰器,而不仅仅是函数装饰器。然后可以像前面一样传入模拟函数:

@patch('mymodule.SomeClass')
class MyTest(TestCase):


def test_one(self, MockSomeClass):
self.assertIs(mymodule.SomeClass, MockSomeClass)

参见: 对每个测试方法应用相同的补丁(其中还列出了替代方案)

如果希望为所有测试方法进行修补,那么在 setUp 上以这种方式设置修补程序更有意义。

如果你有很多补丁要应用,你想让它们应用到 setUp 方法中初始化的东西上,也可以试试这个:

def setUp(self):
self.patches = {
"sut.BaseTestRunner._acquire_slot": mock.Mock(),
"sut.GetResource": mock.Mock(spec=GetResource),
"sut.models": mock.Mock(spec=models),
"sut.DbApi": make_db_api_mock()
}


self.applied_patches = [mock.patch(patch, data) for patch, data in self.patches.items()]
[patch.apply for patch in self.applied_patches]
.
. rest of setup
.




def tearDown(self):
patch.stopall()

我将从回答你们的问题开始,然后给出 patch()setUp()如何相互作用的详细例子。

  1. 我不认为它看起来是正确的,详情请参阅我对问题3的回答。
  2. 是的,实际的补丁调用看起来应该模拟所需的对象。
  3. 不,你几乎从来不想使用 setUp()上的 @patch()装饰器。您很幸运,因为对象是在 setUp()中创建的,并且在测试方法期间从未创建过。
  4. 我不知道有什么方法可以让模拟对象引发异常,而不将该异常导入到您的测试用例文件中。
  5. 我觉得这里不需要 patch.object()。它只允许修补对象的属性,而不是将目标指定为字符串。

为了扩展我对问题 # 3的回答,问题是 patch()装饰器只在装饰函数运行时应用。一旦 setUp()返回,补丁就会被删除。在你的情况下,这是有效的,但我敢打赌,它会迷惑某些人看着这个测试。如果您真的只希望补丁在 setUp()期间发生,我建议使用 with语句来明确表示补丁将被删除。

下面的示例有两个测试用例。TestPatchAsDecorator表明,装饰类将在测试方法期间应用补丁,而不是在 setUp()期间应用补丁。TestPatchInSetUp展示了如何应用补丁,以便在 setUp()和测试方法中都能使用它。调用 self.addCleanUp()确保在 tearDown()期间删除补丁。

import unittest
from mock import patch




@patch('__builtin__.sum', return_value=99)
class TestPatchAsDecorator(unittest.TestCase):
def setUp(self):
s = sum([1, 2, 3])


self.assertEqual(6, s)


def test_sum(self, mock_sum):
s1 = sum([1, 2, 3])
mock_sum.return_value = 42
s2 = sum([1, 2, 3])


self.assertEqual(99, s1)
self.assertEqual(42, s2)




class TestPatchInSetUp(unittest.TestCase):
def setUp(self):
patcher = patch('__builtin__.sum', return_value=99)
self.mock_sum = patcher.start()
self.addCleanup(patcher.stop)


s = sum([1, 2, 3])


self.assertEqual(99, s)


def test_sum(self):
s1 = sum([1, 2, 3])
self.mock_sum.return_value = 42
s2 = sum([1, 2, 3])


self.assertEqual(99, s1)
self.assertEqual(42, s2)

我想指出一个已被接受的答案的变体,在这个答案中,new参数被传递给 patch()装饰器:

from unittest.mock import patch, Mock


MockSomeClass = Mock()


@patch('mymodule.SomeClass', new=MockSomeClass)
class MyTest(TestCase):
def test_one(self):
# Do your test here

注意,在这种情况下,不再需要向每个测试方法添加第二个参数 MockSomeClass,这样可以节省大量代码重复。

对此的解释可以在 https://docs.python.org/3/library/unittest.mock.html#patch上找到:

如果使用 patch()作为修饰符而省略 新的,则创建的 mock 将作为额外参数传递给修饰函数。

上面的答案都省略了 新的,但是可以很方便地包含它。

您可以创建一个打了补丁的内部函数并从 setUp调用它。

如果你原来的 setUp函数是:

def setUp(self):
some_work()

然后你可以把它修补成:

def setUp(self):
@patch(...)
def mocked_func():
some_work()


mocked_func()

解决这个问题的另一种方法是在类之外定义补丁,然后重用它来装饰类以及 setUp 和 tearDown 方法,如下所示(重用 Don Kirkby 的示例) :

import unittest
from mock import patch


my_patch = patch('__builtin__.sum', return_value=99)


@my_patch
class TestPatchAsDecorator(unittest.TestCase):
@my_patch
def setUp(self):
s = sum([1, 2, 3])


self.assertEqual(99, s)


def test_sum(self, mock_sum):
s1 = sum([1, 2, 3])
mock_sum.return_value = 42
s2 = sum([1, 2, 3])


self.assertEqual(99, s1)
self.assertEqual(42, s2)