【问题标题】:How to properly use mock in python with unittest setUp如何通过 unittest setUp 在 python 中正确使用 mock
【发布时间】:2013-03-27 03:11:19
【问题描述】:

在我尝试学习 TDD 的过程中,尝试学习单元测试并在 python 中使用 mock。慢慢掌握它的窍门,但不确定我是否正确执行此操作。预先警告:我坚持使用 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 对象。 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-3) 对我来说似乎很好... 4) import ldap 改为设置side_effect = ldap.INVALID_CREDENTIALS?
  • 您总是可以进行相同的测试,但使用您自己制作的更简单的对象...

标签: python unit-testing mocking


【解决方案1】:

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

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

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

请参阅:Applying the same patch to every test method(其中还列出了替代方案)

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

【讨论】:

  • 我刚刚遇到一个问题,我在 TestCase 类上有一个类级别的模拟,并假设在setUp() 方法中进行调用时它已经到位。不是这种情况;在setUp() 中使用时没有及时应用类级别的模拟。相反,我通过创建一个在所有测试中使用的辅助方法解决了这个问题。不确定这是最好的方法,但它确实有效。
  • @berto 如果您在答案中扩展您的评论,我认为这会有所帮助。与此处的其他解决方案相比,这是一个不同且可能更简单的解决方案。
  • 所以......这个答案是不是完全错误?我现在比在谷歌上搜索之前更加困惑。
【解决方案2】:

我将首先回答您的问题,然后我将详细举例说明patch()setUp() 如何交互。

  1. 我认为它看起来不对,有关详细信息,请参阅此列表中我对问题 #3 的回答。
  2. 是的,对 patch 的实际调用看起来应该模拟您想要的对象。
  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)

【讨论】:

  • 我想您想提供一个指向您提到的答案#3的超链接,因为 SO 会根据他们收到的分数对答案进行排序。
  • 我明白你的意思,@ErdinEray,但我实际上是在谈论我对 OP 对 question #3 的回答。
  • 我真的很喜欢这种技术,因为它允许您为适用于大多数测试的类级模拟创建默认配置,然后需要模拟以不同方式运行的测试可以覆盖它.真的很不错。
【解决方案3】:

我想指出一个已接受答案的变体,其中将 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() 用作装饰器并且省略new,则创建的模拟将作为额外参数传递给装饰函数。

上面所有的答案都省略了new,但包含它会很方便。

【讨论】:

  • 谢谢!这很有帮助,特别是当有许多不需要特殊返回值或类似函数的模拟函数类时。保持测试用例函数定义更简洁。
  • 谢谢,这帮助我使我的代码更干净、更干。我不需要在每个方法的顶部进行注释,因为我有很多模拟类。
  • 这是一个很好的例子,说明为什么人们应该始终滚动查看较旧的 StackOverflow 问题以获得超出公认答案的更多见解!好极了:D
【解决方案4】:

如果你有很多补丁要应用,并且你希望它们应用到在 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()

【讨论】:

  • 考虑在tearDown()中使用patch.stop_all()
  • 我试过这个 - 似乎也需要启动apply_patches。考虑如下一行:for patch in self.applied_patches: patch.start()
  • 这是stopall,而不是stop_all
  • 公平地说——我现在使用“self.addCleanup(patch)”方法。是时候更新这个答案了。
  • self.addCleanup(patch.stopall)
【解决方案5】:

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

如果你原来的setUp函数是:

def setUp(self):
    some_work()

然后您可以通过将其更改为来修补它:

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

    mocked_func()

【讨论】:

  • 为什么不直接使用patch 作为上下文管理器呢? with patch(...):
【解决方案6】:

解决此问题的另一种方法是在类之外定义补丁,然后重用它来装饰类以及 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)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-30
    • 2016-01-26
    相关资源
    最近更新 更多