2017年8月17日 星期四

[Python 文章收集] 用 Mock 來做 Python Unit Test

Source From Here 
Preface 
單元測試可以幫助我們確保開發時有按照目標的規格,並且在程式變動後可以檢查部份變動所造成的影響。Python 這個程式本身就提供了很好的單元測試模組 unittest,這其實已經足夠滿足大部分的測試需求,而到了 python3 時則更進一步將 mock 加入到 unittest 模組中,藉由這個強大的模組我們可以更有效率的來處理單元測試。 


那究竟什麼是 mock 呢?在物件導向程式中,程式設計師可以藉由偽造的物件來替換要執行的部份程式,這樣的作法可以使得測試的目標變得更明確並且與其他程式獨立開來,不會因為其他沒有通過測試的物件而影響到現在測試的目標。下面利用一個簡單的例子來介紹 mock 在單元測試中的好處! 
- example.py 
  1. def func1(x):  
  2.     return x**2  
  3.   
  4. def func2(x):  
  5.     return func1(x) + x*5  
- tests.py 
  1. import unittest  
  2. from example import func1, func2  
  3.   
  4.   
  5. class ExampleTest(unittest.TestCase):  
  6.     """Test example  
  7.     """  
  8.     def test_func2(self):  
  9.         self.assertEqual(func2(5), 50)  
  10.         self.assertEqual(func2(-5), 0)  
在 example 這個模組中 func2 中 會呼叫 func1 並加上 x*5 然後回傳結果,如果要對 func2 做單元測試時,要先確定 func1 這支函式是正確如預期的運作,test_func2 中的 assertEqual 才能成立。在簡單的程式中這樣做自然不是問題,但如果 func1 變得很複雜且又有使用到其他函數時,要怎麼保證 func2 在測試時不會受到 func1 的影響呢?這時候就可以利用 mock 來將我們的目標獨立開來。 
- tests.py 
  1. import unittest  
  2. from unittest import mock  
  3. from example import func1, func2  
  4.   
  5. class ExampleTest(unittest.TestCase):  
  6.     """Test example for mocking  
  7.     """  
  8.     @mock.patch('example.func1')  
  9.     def test_func2(self, mock_func1):  
  10.         mock_func1.return_value = 0  
  11.         self.assertEqual(func2(5), 25)  
  12.         mock_func1.return_value = 10  
  13.         self.assertEqual(func2(-5), -15)  
在 tests.py 中: 
1. 利用 mock 將 example module 中 func1 替換成 mock_func1,之後在 fun2 中碰到 func1 的時候就會變成使用 mock_func1
2. 而在這裡我們定義 mock_func1.return_value = 0,也就是之後不管 func1 的輸入是什麼都只會回傳 0 這個值
3. 藉由這樣的方式就可以把 func1 獨立開來,只要確認 func2 的邏輯是否有符合我們的目標

接下來我們就對 mock 的兩個常用方法做介紹,第一個是 Mock 這個 class,提供很好的方式幫我們偽造一個想要的物件,讓我們自由的訂定輸入和輸出,第二個則是 patch,這個裝飾器幫助我們處理模組層的名稱替換,像第一個範例 func2 會呼叫到 func1,如果我們想要使用 mock_func1,就必須要將模組中的名稱替換成我們的目標,之後 func2 就會去使用我們替換的目標而不是呼叫原本的 func1,而 patch 則幫助我們實踐這一塊。 

MagicMock 
MagicMock 是 Mock 的 subclass,他預先幫我們處理了 python 中的 magic method,如果想要處理一些如 get index 等事情,使用 MagicMock 會方便很多。初始化一個 MagicMock 的物件,利用 return_value 這個 attribute,我們就可以給這個物件一個想要的回傳值,當物件被呼叫時就會回傳我們預先設定好的結果。 
>>> import mock
>>> mock_thing = mock.MagicMock()
>>> mock_thing.return_value = 10
>>> mock_thing()
10

或者我們希望物件能夠照序列的給出結果,這時候就可以使用 side_effect,給予這物件一個 list,之後呼叫物件時就會根據 list 內的順序來回傳: 
>>> mock_thing.side_effect = [1, 2, 3]
>>> for i in range(5):
... print("{}".format(mock_thing()))
...
1
2
3
Traceback (most recent call last):
File "", line 2, in
File "/usr/local/lib/python3.5/site-packages/mock/mock.py", line 1062, in __call__
return _mock_self._mock_call(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/mock/mock.py", line 1121, in _mock_call
result = next(effect)

StopIteration

那如果今天希望物件被呼叫時能得到 exception 該怎麼做呢?只要將 side_effect 指定想要的 exception 就可以了: 
>>> mock_thing.side_effect = Exception('Haha')
>>> mock_thing()
Traceback (most recent call last):
File "", line 1, in
File "/usr/local/lib/python3.5/site-packages/mock/mock.py", line 1062, in __call__
return _mock_self._mock_call(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/mock/mock.py", line 1118, in _mock_call
raise effect
Exception: Haha

又或者想要模擬一個物件的 method 時該怎麼辦做Mock 提供了很簡潔的方式,我們只需要在 mock 物件後面直接使用我們想要的 method name or attribute name: 
>>> mock_thing.some_method.return_value = 5
>>> mock_thing.some_method(1,2,3)
5
>>> mock_thing.some_method.side_effect = ['a', 'b']
>>> mock_thing.some_method()
'a'
>>> mock_thing.some_method()
'b'
>>> mock_thing.some_attribute = 'This is attribute!'
>>> mock_thing.some_attribute
'This is attribute!'

下面介紹幾個在測試中很實用的方法. 

1. called 
這個 attribute 告訴我們 mock 的物件有沒有被呼叫過: 
>>> mock_thing = mock.MagicMock()
>>> mock_thing.called
False
>>> mock_thing()
>>> mock_thing.called
True

2. call_count 
而這則是可以知道 mock 的物件被呼叫了幾次 
>>> mock_thing.some_method2()
>>> mock_thing.some_method2()
>>> mock_thing.some_method2.call_count
2

3. assert_called_with(*args, **kwargs) 
或者我們想測試物件是否有被輸入指定的參數來呼叫,如果沒有則會 trigger AssertionError: 
>>> mock_thing.some_method3(a=1, b=4)
>>> mock_thing.some_method3.assert_called_with(a=1, b=4)
>>> mock_thing.some_method3.assert_called_with(a=1, b=5)
Traceback (most recent call last):
...
raise AssertionError(_error_message()) from cause
AssertionError: Expected call: some_method3(a=1, b=5)
Actual call: some_method3(a=1, b=4)

4. call_args 
列出被呼叫的參數: 
>>> mock_thing.some_method3(a=1, b=4)
>>> mock_thing.some_method3.call_args
call(a=1, b=4)

5. call_args_list 
跟上面很像,不同的是會將曾經被呼叫過的都顯示出來 
>>> mock_thing.some_method3(a=1, b=4)
>>> mock_thing.some_method3(a=1, b=5)
>>> mock_thing.some_method3.call_args_list
[call(a=1, b=4), call(a=1, b=5)]

6. reset_mock 
將 mock 物件重置,要注意的是這個 method 只是清除呼叫的紀錄,對於 reutrn_valueside_effect 不會有影響: 
>>> mock_thing = mock.MagicMock()
>>> mock_thing.return_value = 10
>>> mock_thing()
10
>>> mock_thing.called
True
>>> mock_thing.reset_mock()
>>> mock_thing.called
False
>>> mock_thing()
10

Patch 
接下來介紹 patch 這個 method,竟然 Mock 已經這麼方便了,為什麼還需要 patch 這個東西呢?考慮: 
  1. a.py  
  2. -> Defines SomeClass  
  3. b.py  
  4. -> from a import SomeClass  
  5. -> some_function instantiates SomeClass  
假設 B 模組裡會使用到 A 模組的 class,而這時因為 B 已經有 A 的 reference,如果我們直接 mock SomeClass 是不會有任何影響的,B 模組還是會參考到原本的 A 模組,那應該怎麼做呢?實際上應該是將模組中參照的地方替換成我們的 mock class,我們只需要把他當成 decorator 放在測試案例前面來處理我們想要替換的名稱就可以了,像這樣子: 
  1. @mock.patch('module_name.SomeClassName.some_method_name')  
一個簡單範例如下: 
- example.py: 
  1. def func1(x):  
  2.     return x**2  
  3.   
  4. def func2(x):  
  5.     return func1(x) + x*5  
  6.   
  7. def func3(x):  
  8.     return func1(x) + func2(x) + x*3  
- tests.py: 
  1. import unittest  
  2. from unittest import mock  
  3. from example import func1, func2, func3  
  4.   
  5.   
  6. class ExampleTest(unittest.TestCase):  
  7.     """Test example for patch  
  8.     """  
  9.     @mock.patch('example.func2')  
  10.     @mock.patch('example.func1')  
  11.     def test_func3(self, mock_func1, mock_func2):  
  12.         mock_func1.return_value = 0  
  13.         mock_func2.return_value = 0  
  14.         self.assertEqual(func3(5), 15)  
而 patch 還有像是給特定用法的 patch.objectpatch.dictpatch.multiple 等等,這些可以在官方的說明看到更詳細的用法,最後就用一個實際的例子,來體會一下這強大的工具到底有多方便吧! 

Practice 
透過簡單範例來理解上面的內容: 
- utils.py 
  1. import gzip  
  2.   
  3.   
  4. class Reader(object):  
  5.   
  6.     def __init__(self, filename):  
  7.         self.f = self.open(filename)  
  8.   
  9.     def open(self, filename):  
  10.         try:  
  11.             f = gzip.open(filename, 'rb')  
  12.         except:  
  13.             f = open(filename, 'r')  
  14.         return f  
  15.   
  16.     def get(self):  
  17.         return self.f.readline()  
  18.   
  19.   
  20. def convert(reader):  
  21.     return reader.get().split(',')  
- tests.py 
  1. import unittest  
  2. try:  
  3.     # Python3  
  4.     from unittest import mock  
  5. except:  
  6.     # Python2  
  7.     import mock  
  8. from utils import Reader, convert  
  9.   
  10.   
  11. class ReaderTest(unittest.TestCase):  
  12.   
  13.     @mock.patch('utils.open')  
  14.     @mock.patch('gzip.open')  
  15.     def test_gzip_open(self, mock_gzip, mock_open):  
  16.         mock_gzip.return_value = 'Mock Gzip'  
  17.         reader = Reader('test.csv.gz')  
  18.         mock_gzip.assert_called_with('test.csv.gz''rb')  
  19.         mock_open.assert_not_called()  
  20.         self.assertEqual(reader.f, 'Mock Gzip')  
  21.   
  22.     @mock.patch('utils.open')  
  23.     @mock.patch('gzip.open')  
  24.     def test_builtins_open(self, mock_gzip, mock_open):  
  25.         mock_gzip.side_effect = Exception('Not this')  
  26.         mock_open.return_value = 'Open'  
  27.         reader = Reader('test.csv')  
  28.         mock_gzip.assert_called_with('test.csv''rb')  
  29.         mock_open.assert_called_with('test.csv''r')  
  30.         self.assertEqual(reader.f, 'Open')  
  31.   
  32.     @mock.patch('utils.Reader.open')  
  33.     def test_get(self, mock_open):  
  34.         mock_open.return_value.readline.side_effect = [12]  
  35.         reader = Reader('test.csv')  
  36.         self.assertEqual(reader.get(), 1)  
  37.         self.assertEqual(reader.get(), 2)  
  38.         with self.assertRaises(StopIteration):  
  39.             reader.get()  
  40.   
  41.   
  42. class ConverterTest(unittest.TestCase):  
  43.   
  44.     def test_convert(self):  
  45.         mock_reader = mock.MagicMock()  
  46.         mock_reader.get.return_value = '1,2,3'  
  47.         self.assertEqual(convert(mock_reader), ['1''2''3'])  
  48.   
  49.   
  50. if __name__ == '__main__':  
  51.     unittest.main()  
Supplement 
26.5. unittest.mock — mock object library 
26.6. unittest.mock — getting started 
An Introduction to Mocking in Python 
Python Mock Tutorial

沒有留言:

張貼留言

[ Py DS ] Ch1 - IPython: Beyond Normal Python

Source From  Here   Keyboard Shortcuts in the IPython Shell   If you spend any amount of time on the computer, you’ve probably found a u...