如何在单元测试中正确使用python模拟setUp

问题描述:

在我尝试学习TDD时,试图学习单元测试和使用python进行模拟。慢慢地掌握它,但不确定我是否正确地做到了这一点。预先警告:我使用python 2.4,因为供应商API是预编译的2.4 pyc文件,所以我使用模拟0.8.0和单元测试(不是unittest2)如何在单元测试中正确使用python模拟setUp

鉴于此示例代码在'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)'进口ldap'代替,并设置'side_effect = ldap.INVALID_CREDENTIALS'? – Chris 2013-04-04 22:28:38

+0

您可以随时进行相同的测试,但只能使用自己制作的简单对象... – shackra 2014-05-11 20:09:36

参见:26.5.3.4. Applying the same patch to every test method

更有意义设立该修补上的设置这样,如果你想为所有的测试方法进行修补。

+2

对于Python3之前的模拟(位于http://www.voidspace.org.uk/python/mock/上),请参阅[在每种测试方法上应用相同的补丁](http://www.voidspace.org.uk/python/mock/examples.html#applying-the-same-patch-to-every-test-method)。 – musiphil 2015-03-17 21:59:02

+2

我刚刚遇到了一个问题,我在TestCase类上有一个类级别的模拟,并假定它在'setUp()'方法中进行调用时已经存在。不是这种情况; class级别的mock不能及时应用于'setUp()'中。我解决了这个问题,而是创建了一个我在所有测试中使用的帮助器方法。不知道这是最好的方法,但它的工作原理。 – berto 2016-03-31 15:05:57

+0

@berto如果您在答复中扩充您的评论,我认为这会有所帮助。这是一个不同的,可能比其他人更容易的解决方案。 – KobeJohn 2016-11-17 03:47:01

如果你有很多的补丁应用,并且希望他们申请到的东西在设置方法初始化也试试这个:

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.stop_all() 
+3

考虑在'tearDown()'中使用'patch.stop_all()'。 – 2015-03-30 15:42:09

+0

我试过了 - 似乎apply_patches也需要启动。考虑一下这样的一行:'在self.applied_pa​​tches:patch.start()'中使用补丁 – F1Rumors 2016-04-04 18:44:47

我会回答你的问题开始,然后我就给如何与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)