使用 Python Mock 类进行单元测试

发表于:2017-7-21 13:49

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Garfielt    来源:51Testing软件测试网采编

  数据类型、模型或节点——这些都只是mock对象可承担的角色。但mock在单元测试中扮演一个什么角色呢? 有时,你需要为单元测试的初始设置准备一些“其他”的代码资源。但这些资源兴许会不可用,不稳定,或者是使用起来太笨重。你可以试着找一些其他的资源替代;或者你可以通过创建一个被称为mock的东西来模拟它。Mocks能够让我们模拟那些在单元测试中不可用或太笨重的资源。
  在Python中创建mock是通过Mock模块完成的。你可以通过每次一个属性(one-attribute-at-a-time)或一个健全的字典对象或是一个类接口来创建mock。你还可以定义mock的行为并且在测试过程中检查它的使用。让我们继续探讨。
  除非单独说明,下面的示例代码都是用Python2.4写的。
  测试准备
  典型的测试准备最少有两个部分。首先是测试对象(红色),这是测试的关注点。它可以是一个方法、模块或者类。它可以返回一个结果,也可以不返回结果,但是它可以根据数据数据或者内部状态产生错误或者异常(图1)
  图1
  第二测试用例(灰色),它可以单独运行也可以作为套件的一部分。它是为测试对象准备的,也可以是测试对象需要的任意数据或资源。运行一个或多个测试事务,在每个测试中检查测试对象的行为。收集测试结果并用一个简洁、易读的格式呈现测试结果。
  现在,为了发挥作用,一些测试对象需要一个或多个资源(绿色)。这些资源可以是其他的类或者模块,甚至是一个非独立的进程。不论其性质,测试资源是功能性的代码。他们的角色是支持测试对象,但是他们不是测试的关注点。
  使用Mock的理由
  但是有些时候,测试资源不可用,或者不适合。也许这个资源正在和测试对象并行开发中,或者并不完整或者是太不稳定以至于不可靠。
  测试资源太昂贵,如果测试资源是第三方的产品,其高昂的价格不适用于测试。测试资源的建立过于复杂,占用的硬件和时间可以用于别的地方。如果测试资源是一个数据源,建立它的数据集模仿真实世界是乏味的。
  测试资源是不可预知的。一个好的单元测试是可重复的,允许你分离和识别故障。但是测试资源可能给出随机的结果,或者它会有不同的响应时间。而作为这样的结果,测试资源最终可能成为一个潜在的搅局者。
  这些都是你可能想要用mock代替测试资源的原因。mock向测试对象提供一套和测试资源相同的方法接口。但是mock是更容易创建和管理。它能向测试对象提供和真实的测试资源相同的方法接口。它能提供确定的结果,并可以自定义以适用于特定的测试。能够容易的更新,以反映实际资源的变化。
  当然,mocks不是没有问题的。设计一个精确的mock是困难的,特别是如果你没有测试资源的可靠信息。你可以尝试找到一个开源的接口,或者你能对测试资源的方法接口进行猜测。无论你如何选择,你都可以在以后轻松的更新mock,你可以在首选资源中得到更详细的信息。
  太多的mock会使测试过于复杂,让你跟踪错误变得更困难。最好的实践是每个测试用例限制使用一到两个mock,或者为每个mock/对象对使用独立的测试用例。
  Mocks对Stubs对Fakes
  Mock不是模仿测试资源的唯一方式。其他的解决方案如stub和fake也能提供相同的服务。因此,mock和其他两种解决方案怎样比较?为什么选择mock而不是选择stub或者fake?
  认识stub:stub为测试对象提供了一套方法接口,和真实的测试资源提供给测试对象的接口是相同的。当测试对象调用stub方法时,stub响应预定的结果。也可以产生一个预定的错误或者异常。stub可以跟踪和测试对象的交互,但是它不处理输入的数据。
  fake也提供了一套方法接口并且也可以跟踪和测试对象的交互。但是和stub不同,fake真正的处理了从测试对象输入的数据产生的结果是基于这些数据的。简而言之,fake是功能性的,它是真实测试资源的非生产版。它缺乏资源的相互制衡,使用了更简单的算法,而且它很少存储和传输数据。
  使用fake和stub,你可以输入正确的数据调用了正确的方法对测试对象进行测试。你能测试对象是如何处理数据并产生结果,当出现错误或者异常时是怎样反应的。这些测试被称为状态验证。但是你是否想知道测试对象调用了两次相同的方法?你是否想知道测试对象是否按照正确的顺序调用了几个方法?这种测试被称为行为验证,而要做到这些,你需要mocks。
  使用Python Mock
  在Python中Mock模块是用来创建和管理mock对象的。该模块是Michael Foord的心血结晶,它是Python3.0的标准模块。因此在Python2.4~2.7中,你不得不自己安装这个模块。你可以 Python Package Index website从获得Mock模块最新的版本。
  基于你的mock对象,Mock模块提供了少量的类。为了改变运行中的mock甚至提供了补丁机制。但是现在,我们关注一个类:Mock类。
  图2中显示了Mock类(绿色)的基本结构。它继承于两个父类:NonCallableMock和CallableMixin(灰色)。NonCallableMock定义了mock对象所需的例程。它重载了几个魔法方法,给他们定义了缺省的行为。为了跟踪mock的行为,它提供了断言例程。CallableMixin更新了mock对象回调的魔法方法。反过来,两个父类继承于Base类(红色),声明了mock对象所需的属性。
  图2
  准备Mock
  Mock类有四套方法(图3)。第一套方法是类的构造器,它有六个可选和已标记的参数。图中显示了4个经常用到的参数。
  图3
  构造器的第一个参数是name,它定义了mock对象的唯一标示符。Listing one显示了怎么创建一个标示符为Foo的mock对象mockFoo。请注意当我打印mock对象(6-9行)时,标示符后紧跟的是mock对象的唯一ID。
  Listing On
  from mock import Mock
  # create the mock object
  mockFoo = Mock(name = "Foo")
  print mockFoo
  # returns: <Mock name='Foo' id='494864'>
  print repr(mockFoo)
  # still returns: <Mock name='Foo' id='494864'>
  构造器的第二个参数是spec。它设置mock对象的属性,可以是property或者方法。属性可以是一个列表字符串或者是其他的Python类。
  为了演示,在Listing Two中,我有一个带三个项目的列表对象fooSpec(第4行):property属性_fooValue,方法属性callFoo和doFoo。当我把fooSpec带入类构造器时(第7行),mockFoo获得了三个属性,我能用点操作符访问它们(10~15行)。当我访问了一个未声明的属性时,mockFoo引发AttributeError和"faulty"属性(21~14行)。
  Listing Two
  from mock import Mock
   
  # prepare the spec list
  fooSpec = ["_fooValue", "callFoo", "doFoo"]
   
  # create the mock object
  mockFoo = Mock(spec = fooSpec)
   
  # accessing the mocked attributes
  print mockFoo
  # <Mock id='427280'>
  print mockFoo._fooValue
  # returns <Mock name='mock._fooValue' id='2788112'>
  print mockFoo.callFoo()
  # returns: <Mock name='mock.callFoo()' id='2815376'>
   
  mockFoo.callFoo()
  # nothing happens, which is fine
   
  # accessing the missing attributes
  print mockFoo._fooBar
  # raises: AttributeError: Mock object has no attribute '_fooBar'
  mockFoo.callFoobar()
  # raises: AttributeError: Mock object has no attribute 'callFoobar
  Listing Three显示了spec参数的另一种用法。这次,有带三个属性的类Foo(4~12行)。把类名传入构造器中,这样就产生了一个和Foo有相同属性的mock对象(18~23行)。再次,访问一个未声明的属性引发了AttributeError(29~32行)。也就是说,在两个例子中,方法属性时没有功能的。甚至在方法有功能代码时,调用mock的方法也什么都不做。
  Listing Three
  from mock import Mock
   
  # The class interfaces
  class Foo(object):
      # instance properties
      _fooValue = 123
       
      def callFoo(self):
          print "Foo:callFoo_"
       
      def doFoo(self, argValue):
          print "Foo:doFoo:input = ", argValue    
   
  # create the mock object
  mockFoo = Mock(spec = Foo)
   
  # accessing the mocked attributes
  print mockFoo
  # returns <Mock spec='Foo' id='507120'>
  print mockFoo._fooValue
  # returns <Mock name='mock._fooValue' id='2788112'>
  print mockFoo.callFoo()
  # returns: <Mock name='mock.callFoo()' id='2815376'>
   
  mockFoo.callFoo()
  # nothing happens, which is fine
   
  # accessing the missing attributes
  print mockFoo._fooBar
  # raises: AttributeError: Mock object has no attribute '_fooBar'
  mockFoo.callFoobar()
  # raises: AttributeError: Mock object has no attribute 'callFoobar'
  下一个构造器参数是return_value。这将设置mock对象的响应当它被直接调用的时候。我用这个参数模拟一个工厂调用。
  为了演示,在Listing Four中,设置return_value为456(第4行)。当调用mockFoo时,将返回456的结果(9~11行)。在Listing Five中,我给return_value传入了一个类Foo的实例fooObj(15~19行)。现在,当我调用mockFoo时,我获得了fooObj的实例(显示为mockObj)(第24行)。和Listing Two和Three不一样,mockObj的方法是带有功能的。
  Listing Four
  from mock import Mock
   
  # create the mock object
  mockFoo = Mock(return_value = 456)
   
  print mockFoo
  # <Mock id='2787568'>
   
  mockObj = mockFoo()
  print mockObj
  # returns: 456
  Listing Five
  from mock import Mock
   
  # The mock object
  class Foo(object):
      # instance properties
      _fooValue = 123
       
      def callFoo(self):
          print "Foo:callFoo_"
       
      def doFoo(self, argValue):
          print "Foo:doFoo:input = ", argValue
   
  # creating the mock object
  fooObj = Foo()
  print fooObj
  # returns: <__main__.Foo object at 0x68550>
   
  mockFoo = Mock(return_value = fooObj)
  print mockFoo
  # returns: <Mock id='2788144'>
   
  # creating an "instance"
  mockObj = mockFoo()
  print mockObj
  # returns: <__main__.Foo object at 0x68550>
   
  # working with the mocked instance
  print mockObj._fooValue
  # returns: 123
  mockObj.callFoo()
  # returns: Foo:callFoo_
  mockObj.doFoo("narf")
  # returns: Foo:doFoo:input =  narf
  <Mock id='428560'>
  side_effect参数和return_value是相反的。它给mock分配了可替换的结果,覆盖了return_value。简单的说,一个模拟工厂调用将返回side_effect值,而不是return_value。
  Listing Six演示了side_effect参数的影响。首先,创建类Foo的实例fooObj,把它传入return_value参数(第17行)。这个结果和Listing Five是类似的。当它被调用的时候,mockFoo返回fooObj(第22行)。然后我重复同样的步骤,给side_effect参数传入StandardError(第28行),现在,调用mockFoo引发了StandardError,不再返回fooObj(29~30行)。
  Listing Six
  from mock import Mock
   
  # The mock object
  class Foo(object):
      # instance properties
      _fooValue = 123
       
      def callFoo(self):
          print "Foo:callFoo_"
       
      def doFoo(self, argValue):
          print "Foo:doFoo:input = ", argValue
   
  # creating the mock object (without a side effect)
  fooObj = Foo()
   
  mockFoo = Mock(return_value = fooObj)
  print mockFoo
  # returns: <Mock id='2788144'>
   
  # creating an "instance"
  mockObj = mockFoo()
  print mockObj
  # returns: <__main__.Foo object at 0x2a88f0>
   
  # creating a mock object (with a side effect)
   
  mockFoo = Mock(return_value = fooObj, side_effect = StandardError)
  mockObj = mockFoo()
  # raises: StandardError
  Listing Seven显示了另一个影响。在这个例子中,传入一个列表对象fooList到类构造器中(17~18行)。然后,每次我调用mockFoo时,它连续的返回列表中的项(20~30行)。一旦mockFoo到达了列表的末尾,调用将引发StopIteration 错误(32~34行)
  Listing Seven
  from mock import Mock
   
  # The mock object
  class Foo(object):
      # instance properties
      _fooValue = 123
       
      def callFoo(self):
          print "Foo:callFoo_"
       
      def doFoo(self, argValue):
          print "Foo:doFoo:input = ", argValue
   
  # creating the mock object (with a side effect)
  fooObj = FooSpec()
   
  fooList = [665, 666, 667]
  mockFoo = Mock(return_value = fooObj, side_effect = fooList)
   
  fooTest = mockFoo()
  print fooTest
  # returns 665
   
  fooTest = mockFoo()
  print fooTest
  # returns 666
   
  fooTest = mockFoo()
  print fooTest
  # returns 667
   
  fooTest = mockFoo()
  print fooTest
  # raises: StopIteration
  你可以传入其他的可迭代对象(集合,元组)到side_effct对象中。你不能传入一个简单对象(如整数、字符串等),因为这些对象是不能迭代的,为了让简单对象可迭代,需要将他们加入单一项的列表中。
  Mock断言
  Mock类的下一套方法是断言。它将帮助跟踪测试对象对mock方法的调用。他们能和unittest模块的断言一起工作。能连接到mock或者其方法属性之一。 有两个相同的可选参数:一个变量序列,一个键/值序列。
  第一个断言assert_called_with(),检查mock方法是否获得了正确的参数。当至少一个参数有错误的值或者类型时,当参数的数量错误时,当参数的顺序错误时,或者当mock的方法根本不存在任何参数时,这个断言将引发错误。Listing Eight显示了可以怎样使用这个断言。那儿,我准备了一个mock对象,用类Foo作为它的spec参数。我调用了类的方法doFoo(),传入了一个字符串作为输入。使用assert_called_with(),我检查方法是否获得了正确的输入。第20行的断言通过了,因为doFoo()获得了"narf"的输入。但是在第24行的断言失败了因为doFoo()获得了"zort",这是错误的输入。
  Listing Eight
  from mock import Mock
   
  # The mock object
  class Foo(object):
      # instance properties
      _fooValue = 123
       
      def callFoo(self):
          pass
       
      def doFoo(self, argValue):
          pass
   
  # create the mock object
  mockFoo = Mock(spec = Foo)
  print mockFoo
  # returns <Mock spec='Foo' id='507120'>
   
  mockFoo.doFoo("narf")
  mockFoo.doFoo.assert_called_with("narf")
  # assertion passes
   
  mockFoo.doFoo("zort")
  mockFoo.doFoo.assert_called_with("narf")
  # AssertionError: Expected call: doFoo('narf')
  # Actual call: doFoo('zort')
  Listing Nine显示了稍微不同的用法。在这个例子中,我调用了mock方法callFoo(),首先没有任何输入,然后输入了字符串“zort”。第一个断言通过了(第20行),因为callFoo()不支持获得任何输入。而第二个断言失败了(第24行)因为显而易见的原因。
  Listing Nine
  from mock import Mock
   
  # The mock object
  class Foo(object):
      # instance properties
      _fooValue = 123
       
      def callFoo(self):
          pass
       
      def doFoo(self, argValue):
          pass
   
  # create the mock object
  mockFoo = Mock(spec = Foo)
  print mockFoo
  # returns <Mock spec='Foo' id='507120'>
   
  mockFoo.callFoo()
  mockFoo.callFoo.assert_called_with()
  # assertion passes
   
  mockFoo.callFoo("zort")
  mockFoo.callFoo.assert_called_with()
  # AssertionError: Expected call: callFoo()
  # Actual call: callFoo('zort')
  先一个断言是assert_called_once_with()。像assert_called_with()一样,这个断言检查测试对象是否正确的调用了mock方法。但是当同样的方法调用超过一次时, assert_called_once_with()将引发错误,然而assert_called_with()会忽略多次调用。Listing Ten显示了怎样使用这个断言。那儿,我调用了mock方法callFoo()两次。第一次调用时(行19~20),断言通过。但是在第二次调用的时(行23~24),断言失败,发送了错误消息到stdout。
  Listing Ten
  from mock import Mock
   
  # The mock object
  class Foo(object):
      # instance properties
      _fooValue = 123
       
      def callFoo(self):
          pass
       
      def doFoo(self, argValue):
          pass
   
  # create the mock object
  mockFoo = Mock(spec = Foo)
  print mockFoo
  # returns <Mock spec='Foo' id='507120'>
   
  mockFoo.callFoo()
  mockFoo.callFoo.assert_called_once_with()
  # assertion passes
   
  mockFoo.callFoo()
  mockFoo.callFoo.assert_called_once_with()
  # AssertionError: Expected to be called once. Called 2 times.
  断言assert_any_call(),检查测试对象在测试例程中是否调用了测试方法。它不管mock方法和断言之间有多少其他的调用。和前面两个断言相比较,前两个断言仅检查最近一次的调用。
  Listing Eleven显示了assert_any_call()断言如何工作:仍然是同样的mock对象,spec参数是Foo类。第一个调用方法callFoo()(第18行),接下来调用两次doFoo()(行19~20)。注意doFoo()获得了两个不同的输入。
  Listing Eleven
  <from mock import Mock
   
  # The mock specification
  class Foo(object):
      _fooValue = 123
       
      def callFoo(self):
          pass
       
      def doFoo(self, argValue):
          pass
   
  # create the mock object
  mockFoo = Mock(spec = Foo)
  print mockFoo
  # returns <Mock spec='Foo' id='507120'>
   
  mockFoo.callFoo()
  mockFoo.doFoo("narf")
  mockFoo.doFoo("zort")
   
  mockFoo.callFoo.assert_any_call()
  # assert passes
   
  mockFoo.callFoo()
  mockFoo.doFoo("troz")
   
  mockFoo.doFoo.assert_any_call("zort")
  # assert passes
   
  mockFoo.doFoo.assert_any_call("egad")
  # raises: AssertionError: doFoo('egad') call not found
  第一个assert_any_call()(第22行)通过,虽然两次doFoo()调用隔开了断言和callFoo()。第二个断言(第28行)也通过了,虽然一个callFoo()隔开了我们提到的doFoo()(第20行)。另一方面,第三个断言(第31行)失败了,因为没有任何doFoo()的调用使用了"egad"的输入。
  最后,还有assert_has_calls()。它查看方法调用的顺序,检查他们是否按正确的次序调用并带有正确的参数。它带有两个参数:期望调用方法的列表和一个可选悬殊any_order。当测试对象调用了错误的方法,调用了不在次序中的方法,或者方法获得了一个错误的输入,将生产断言错误。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号