关于单元测试:如何在python中使用unittest setUp正确使用mock

How to properly use mock in python with unittest setUp

在我尝试学习TDD时,尝试学习单元测试并使用mock与python。慢慢地掌握它,但不确定我是否正确地这样做。预警:我正在坚持使用python 2.4,因为供应商API是预先编译的2.4 pyc文件,所以我使用模拟0.8.0和unittest(不是unittest2)

给出'mymodule.py'中的示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,所以我认为这是我必须嘲笑的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

引出了几个问题:

  • 那看起来不错吗? :)
  • 这是尝试和模拟在我正在测试的类中实例化的对象的正确方法吗?
  • 可以在setUp上调用@patch装饰器,还是会导致奇怪的副作用?
  • 反正是否有模拟提升ldap.INVALID_CREDENTIALS异常而不必将异常导入我的testcase文件?
  • 我应该使用patch.object()而不是,如果是这样,怎么样?
  • 谢谢。


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

    1
    2
    3
    4
    5
    @patch('mymodule.SomeClass')
    class MyTest(TestCase):

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

    见:26.5.3.4。将相同的补丁应用于每种测试方法(也列出替代方案)

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


    我将首先回答您的问题,然后我将详细介绍patch()setUp()如何进行交互。

  • 我不认为它看起来是正确的,请参阅此列表中问题#3的答案以获取详细信息。
  • 是的,实际调用补丁看起来应该模拟你想要的对象。
  • 不,你几乎不想在setUp()上使用@patch()装饰器。你很幸运,因为该对象是在setUp()中创建的,并且在测试方法期间永远不会被创建。
  • 我不知道如何使模拟对象引发异常而不将该异常导入测试用例文件。
  • 我认为这里不需要patch.object()。它只是允许您修补对象的属性,而不是将目标指定为字符串。
  • 为了扩展我对问题#3的回答,问题是patch()装饰器仅在装饰函数运行时应用。只要setUp()返回,就会删除补丁。在你的情况下,这是有效的,但我敢打赌,这会让看到这个测试的人感到困惑。如果您真的只想在setUp()期间发生补丁,我建议使用with语句来明确补丁将被删除。

    以下示例有两个测试用例。 TestPatchAsDecorator表明装饰类将在测试方法期间应用补丁,但不会在setUp()期间应用补丁。 TestPatchInSetUp显示了如何在setUp()和测试方法中应用补丁以使其适用。调用self.addCleanUp()可确保在tearDown()期间删除补丁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    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)


    如果要应用许多补丁并且希望它们应用于setUp方法中初始化的内容,请尝试以下操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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()


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

    1
    2
    3
    4
    5
    6
    7
    8
    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找到对此的解释:

    If patch() is used as a decorator and new is omitted, the created mock is passed in as an extra argument to the decorated function.

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


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

    如果您的原始setUp功能是:

    1
    2
    def setUp(self):
        some_work()

    然后你可以修改它来修改它:

    1
    2
    3
    4
    5
    6
    def setUp(self):
        @patch(...)
        def mocked_func():
            some_work()

        mocked_func()