1. 程式人生 > >[OpenStack UT] 分析OpenStack中單元測試之mock & mox

[OpenStack UT] 分析OpenStack中單元測試之mock & mox

在社群貢獻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