如何在带有unittest setUp的python中正确使用模拟


72

在尝试学习TDD的过程中,尝试学习单元测试并在python中使用模拟。慢慢掌握它,但不确定我是否正确执行此操作。预先警告:由于供应商API是预编译的2.4 pyc文件,因此我正在使用python 2.4进行卡住,因此我正在使用模拟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对象。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. 无论如何,有没有要引发ldap.INVALID_CREDENTIALS异常的模拟而不必将异常导入到我的测试用例文件中?
  5. 我应该改用patch.object(),如果是,怎么办?

谢谢。


1
1-3)对我来说似乎很好... 4)import ldap而是设置side_effect = ldap.INVALID_CREDENTIALS
克里斯,

您始终可以进行相同的测试,但使用自己制作的对象更简单...
shackra 2014年

Answers:


76

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

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

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

请参阅:将相同的补丁程序应用于每种测试方法(还列出了替代方法)

如果要对所有测试方法进行修补,则在setUp上以这种方式设置修补程序更有意义。


11
我只是遇到一个问题,我在TestCase类上有一个类级别的模拟,并假定在setUp()方法中进行调用时该模拟已经存在。不是这种情况; 不能及时应用类级别的模拟,以便在中使用setUp()。我通过创建在所有测试中使用的辅助方法解决了该问题。不确定这是最好的方法,但它是否有效。
berto

@berto如果您在答案中扩展评论,我认为这会有所帮助。与这里的其他解决方案相比,这是一个不同的解决方案,可能更容易。
科比·约翰

19

我将首先回答您的问题,然后再给出有关如何patch()进行setUp()交互的详细示例。

  1. 我认为它看起来不正确,有关详细信息,请参见此列表中对问题3的回答。
  2. 是的,实际的补丁调用看起来应该模拟了您想要的对象。
  3. 不,您几乎永远不想在上使用@patch()装饰器setUp()。您很幸运,因为对象是在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)

我想您想给您提到的答案#3提供超链接,因为SO会根据他们收到的分数对答案进行排序。
Erdin Eray

我明白了你的意思,@ ErdinEray,但我实际上是在谈论我对OP的问题3的回答。
Don Kirkby

我真的很喜欢这种技术,因为它允许您为适用于大多数测试的类级模拟创建默认配置,然后需要该模拟以不同方式运行的测试可以覆盖该配置。真的很好。
亚当·帕金

13

如果您有许多补丁要应用,并且希望它们也可以应用到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()

5
考虑使用patch.stop_all()tearDown()
米歇尔·达米科2015年

1
我试过了-似乎还需要启动Applied_pa​​tches。考虑这样的一行:for patch in self.applied_patches: patch.start()
F1Rumors '16

stopall,不是stop_all
OrangeDog

3
公平地说-我现在将使用“ self.addCleanup(patch)”方法。是时候更新这个答案了。
丹尼·史泰普

self.addCleanup(patch.stopall)
耶瑟

11

我想指出一种可接受的答案的变体,其中将一个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中找到:

如果将ifpatch()用作装饰器,并且省略new,则将创建的模拟作为额外的参数传递给装饰函数。

首先,答案省略new,但是将其包括在内可能很方便。


1
谢谢你!这很有帮助,尤其是当有许多模拟的函数类不需要特殊的返回值或类似的值时。保持测试用例功能定义的简洁。
Asmo Soinio '18 -10-15

谢谢,这帮助我使代码更整洁,更干燥。我有很多模拟类,因此不需要在每个方法的顶部进行注释。
Sukhinderpal Mann

0

您可以创建一个修补的内部函数,然后从中调用它setUp

如果您的原始setUp功能是:

def setUp(self):
    some_work()

然后,您可以将其更改为:

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

    mocked_func()

那么为什么不仅仅patch用作上下文管理器呢?with patch(...):
基金莫妮卡的诉讼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.