当前位置: 首页 > 工具软件 > Mox > 使用案例 >

[OpenStack UT] 分析OpenStack中单元测试之mock & mox

秦彦君
2023-12-01

在社区贡献OpenStackcode时,会经常短短的几行代码也要添加不少的UT,耗时耗力,mock & mox 是很好的实现隔离的单元测试模块, 理解它们能够更快的做UT的编码。

mock & mox

都是python中用于实现单元测试的module库, 实现的是隔离, 它通过替换测试内容中的一部分(比如class, function等object).  专注在function的核心实现逻辑的测试上. 比如把db操作, I/O, 网络相关操所如socket, ssh等隔离掉, 在测试运行过程中, 当执行到它们时, 不会深入它们方法内部去执行, 而是直接返回我们假设的一个值。

mock的设计与我们知道mox实现框架不一样, mock是'action->assertion' 方式, mox是典型的'record -> replay->verify'方式, 以例子来分析

mock :

下面这个简单例子摘在mock源码包自带的使用介绍上

>>> from mock import Mock
>>> real = ProductionClass()
>>> real.method = Mock(return_value=3)
>>> real.method(3, 4, 5, key='value')
3
>>> real.method.assert_called_with(3, 4, 5, key='value')

前四行为action, 最后一行assertion,检查是否被调用, 并且可以同时检查调用的参数是否正确.

mox:

mox 的工作方式可以很清晰的在mox 发行的package中看代码,mox.py中可以得到很清晰的理解, 使用一般会经历三种模式的工作状态, 具体的测试时,有时第三种模式工作状态没有用到。

record mode: 创建一个mock对象, 设置mock对象的期望行为, 如返回的值, 传入参数及其顺序.

replay mode: 这一步开始真正的测试, 执行我们测试的方法

verify mode: 这一步开始校验我们在recode mode设置的一些行为是否被期望的执行, 执行是否正确.

关于mox创建mock object的使用是非线程安全的, 在多线程情况下调用需要使用互斥锁.

以下示例摘自mox.py, 简易清晰的讲述了mox工作流程, 3-13行为recode mode, 14-19行为replay mode, 19行之后为verify mode. 当然实际使用过程中很少这样简单, 会用到mox中提供的其他功能. 之后会讲到。

Suggested usage / workflow:

  # Create Mox factory
  my_mox = Mox()

  # Create a mock data access object
  mock_dao = my_mox.CreateMock(DAOClass)

  # Set up expected behavior
  mock_dao.RetrievePersonWithIdentifier('1').AndReturn(person)
  mock_dao.DeletePerson(person)

  # Put mocks in replay mode
  my_mox.ReplayAll()

  # Inject mock object and run test
  controller.SetDao(mock_dao)
  controller.DeletePersonById('1')

  # Verify all methods were called as expected
  my_mox.VerifyAll()

接下来介绍下OpenStack中关于这两种python 测试框架的常见使用

1. [mox] StubOutWithMock

在OpenStack的test code中我们经常看到mox中使用StubOutWithMock, 这个function做的事情就是用mock对象替掉一些方法或者属性. 见下例,摘自OpenStack中代码片段,用来说明StubOutWithMock用法。

   self.mox.StubOutWithMock(quota.QUOTAS, "reserve") #quota.QUOTAS.reserve替换为mock对象
   quota.QUOTAS.reserve(self.context, instances=40,cores=mox.IsA(int),ram=mox.IsA(int)
   ).AndRaise(quota_exception) #mox.IsA是测试基本数据或对象类型的
   self.mox.ReplayAll()
   function(...) #这里简单化了这个方法, 它是调用过quota.QUOTAS.reserve的一个function,
                 #也即是我们需要测试的function

2.[mock] mock.patch & mock.patch.object

  这两者用于把对象class, function, attribute  做成一个mock.

  从定义上就可以看出两者之间的差别

   mock.patch(target, ...)

   mock.patch.object(target, attribute, ...)

   前者给target做mock, 这个target可以是class, function

   后者给target上的attribute作mock, 一般使用中用的比较多的是对象上的function. 

具体实现上, 两者都可以有两种写法, 一种叫decorators, 利用python中的装饰@, 另一种用with语句, 下面是随便写的一个例子

随便定义一个简单的func()来被测试

class A(object):    
    def func(self):
        return B.getFromDB('b')
        
class B(object):
    def getFromDB(self, param):
        ...

具体的测试方法如下

"""第一种写法, decorators方式, 采用mock.patch模拟一个方法.
          当有多个方法时注意一点就是参数的顺序, 如
   @mock.patch('B.getFromDB')
   @mock.patch('B.getFromXX')
   def test_func(self, mock_getFromXX, mock_getFromDB):
   可以看到后@的对象在参数列表中在前面, 这个是由于@的执行顺序为@mock.patch('B.getFromDB')(@mock.patch('B.getFromXX')(func)) 决定的
"""        
@mock.patch('B.getFromDB')
def test_func(self, mock_getFromDB):
    mock_getFromDB.return_value = '1'
    self.assertEqual('1', A.func())

"""第二种写法 ,decorators方式, 采用mock.patch. object模拟一个方法.
"""   
@mock.patch.object(B, 'getFromDB')
def test_func(self, mock_getFromDB):
    mock_getFromDB.return_value = '1'
    self.assertEqual('1', A.func())

"""第三种写法, with statement, 采用mock.patch模拟一个方法.
"""   
def test_func(self, mock_getFromDB):
    with mock.patch('B.getFromDB') as mock_getFromDB:
        mock_getFromDB.return_value = '1'
    self.assertEqual('1', A.func())

"""第四种写法, with statement, 采用mock.patch.object模拟一个方法.
"""   
def test_func(self, mock_getFromDB):
    with mock.patch.object(B, 'getFromDB') as mock_getFromDB:
        mock_getFromDB.return_value = '1'
    self.assertEqual('1', A.func())


上面的例子中, 关于mock.patch模拟的都是function, 没有针对class, 下面是mock.patch document 给出的例子,

>>> class Class(object):
...     def method(self):
...         pass
...
>>> with patch('__main__.Class') as MockClass:
...     instance = MockClass.return_value #本例中模拟了'__main__.Class'这个class, 现把模拟结果赋给intance来测试
...     instance.method.return_value = 'foo' #模拟对象和原class有相同的atribute, 故模拟对象也由method()函数, 可以模拟其返回值
...     assert Class() is instance
...     assert Class().method() == 'foo'
它模拟class, 在OpneStack各个component中现在用的不多 .

3.[mock] side_effect

上面的例子中都是使用模拟对象的return_value, 其实side_effect也经常用, 经常使用它的时候是模拟抛异常 或者模拟一个数据序列.

抛异常

下面是个抛异常的例子 模拟cinder.backup.API的restore方法, 并设置当它被调用时, 抛出异常exception.VolumeSizeExceedsAvailableQuota(requested='2',            consumed='2',quota='3')

    @mock.patch('cinder.backup.API.restore')
    def test_restore_backup_with_VolumeSizeExceedsAvailableQuota(
            self,
            _mock_backup_restore):

        _mock_backup_restore.side_effect = \
            exception.VolumeSizeExceedsAvailableQuota(requested='2',
                                                      consumed='2',
                                                      quota='3')

模拟数据序列,

这种主要用于在一个test里面, 测试过程中多次调用到模拟的对象时, 需要按照调用先后顺序分别给予不同的返回值的情况, 一个很好的例子, 摘自cinder项目的test_backups.py,用来讲述模拟一个数据序列的用法

    """省略了几个测试用例, 主要讲两个,
       backup_api._is_backup_service_enabled会调用cinder.db.service_get_all_by_topic,
       使用side_effect 后, 可以多次测试_is_backup_service_enabled模拟不同情况
       每次_is_backup_service_enabled调用到service_get_all_by_topic时, 模拟的返回值为设置的数据列表中的next one.
    """
    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_is_backup_service_enabled(self, _mock_service_get_all_by_topic):

        test_host = 'test_host'
        alt_host = 'strange_host'
        empty_service = []
        #service host not match with volume's host
        host_not_match = [{'availability_zone': "fake_az", 'host': alt_host,
                           'disabled': 0, 'updated_at': timeutils.utcnow()}]
        ...
        #Setup mock to run through the following service cases
        _mock_service_get_all_by_topic.side_effect = [empty_service,
                                                      host_not_match,
                                                      ...]
        volume_id = utils.create_volume(self.context, size=2,host=test_host)['id']
        volume = self.volume_api.get(context.get_admin_context(), volume_id)
        #test empty service
        self.assertEqual(self.backup_api._is_backup_service_enabled(volume, test_host), False)
        #test host not match service
        self.assertEqual(self.backup_api._is_backup_service_enabled(volume, test_host), False)

4.[mock] MagicMock

也可以看到有些地方使用到了MagicMock, 它是Mock的子类, 它首先是实现了Mock的所有功能,它的更好的地方在于它自动实现大部分magic method(这些方法以双下划线开头的, 详细了解见http://www.ironpythoninaction.com/magic-methods.html)的模拟了,省去了我们使用它们时额外需要的设置。比如一些magic method的return value就有默认的值,如下所示,这些摘自mock 自带的文档中,
    __int__ : 1
    __contains__ : False
    __len__ : 1
    __iter__ : iter([])
    __exit__ : False
    __complex__ : 1j
    __float__ : 1.0
    __bool__ : True
    __nonzero__ : True
    __oct__ : ‘1’
    __hex__ : ‘0x1’
    __long__ : long(1)
    __index__ : 1
    __hash__ : default hash for the mock
    __str__ : default str for the mock
    __unicode__ : default unicode for the mock
    __sizeof__: default sizeof for the mock

 类似资料: