Source From Here
Preface 單元測試可以幫助我們確保開發時有按照目標的規格,並且在程式變動後可以檢查部份變動所造成的影響。Python 這個程式本身就提供了很好的單元測試模組 unittest,這其實已經足夠滿足大部分的測試需求,而到了 python3 時則更進一步將 mock 加入到 unittest 模組中,藉由這個強大的模組我們可以更有效率的來處理單元測試。
那究竟什麼是 mock 呢?在物件導向程式中,程式設計師可以藉由偽造的物件來替換要執行的部份程式,這樣的作法可以使得測試的目標變得更明確並且與其他程式獨立開來,不會因為其他沒有通過測試的物件而影響到現在測試的目標。下面利用一個簡單的例子來介紹 mock 在單元測試中的好處!
- example.py
- def func1(x):
- return x**2
- def func2(x):
- return func1(x) + x*5
- import unittest
- from example import func1, func2
- class ExampleTest(unittest.TestCase):
- """Test example
- """
- def test_func2(self):
- self.assertEqual(func2(5), 50)
- self.assertEqual(func2(-5), 0)
- tests.py
- import unittest
- from unittest import mock
- from example import func1, func2
- class ExampleTest(unittest.TestCase):
- """Test example for mocking
- """
- @mock.patch('example.func1')
- def test_func2(self, mock_func1):
- mock_func1.return_value = 0
- self.assertEqual(func2(5), 25)
- mock_func1.return_value = 10
- self.assertEqual(func2(-5), -15)
接下來我們就對 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,我們就可以給這個物件一個想要的回傳值,當物件被呼叫時就會回傳我們預先設定好的結果。
或者我們希望物件能夠照序列的給出結果,這時候就可以使用 side_effect,給予這物件一個 list,之後呼叫物件時就會根據 list 內的順序來回傳:
那如果今天希望物件被呼叫時能得到 exception 該怎麼做呢?只要將 side_effect 指定想要的 exception 就可以了:
又或者想要模擬一個物件的 method 時該怎麼辦做?Mock 提供了很簡潔的方式,我們只需要在 mock 物件後面直接使用我們想要的 method name or attribute name:
下面介紹幾個在測試中很實用的方法.
1. called
這個 attribute 告訴我們 mock 的物件有沒有被呼叫過:
2. call_count
而這則是可以知道 mock 的物件被呼叫了幾次
3. assert_called_with(*args, **kwargs)
或者我們想測試物件是否有被輸入指定的參數來呼叫,如果沒有則會 trigger AssertionError:
4. call_args
列出被呼叫的參數:
5. call_args_list
跟上面很像,不同的是會將曾經被呼叫過的都顯示出來
6. reset_mock
將 mock 物件重置,要注意的是這個 method 只是清除呼叫的紀錄,對於 reutrn_value,side_effect 不會有影響:
Patch
接下來介紹 patch 這個 method,竟然 Mock 已經這麼方便了,為什麼還需要 patch 這個東西呢?考慮:
- a.py
- -> Defines SomeClass
- b.py
- -> from a import SomeClass
- -> some_function instantiates SomeClass
- @mock.patch('module_name.SomeClassName.some_method_name')
- example.py:
- def func1(x):
- return x**2
- def func2(x):
- return func1(x) + x*5
- def func3(x):
- return func1(x) + func2(x) + x*3
- import unittest
- from unittest import mock
- from example import func1, func2, func3
- class ExampleTest(unittest.TestCase):
- """Test example for patch
- """
- @mock.patch('example.func2')
- @mock.patch('example.func1')
- def test_func3(self, mock_func1, mock_func2):
- mock_func1.return_value = 0
- mock_func2.return_value = 0
- self.assertEqual(func3(5), 15)
Practice
透過簡單範例來理解上面的內容:
- utils.py
- import gzip
- class Reader(object):
- def __init__(self, filename):
- self.f = self.open(filename)
- def open(self, filename):
- try:
- f = gzip.open(filename, 'rb')
- except:
- f = open(filename, 'r')
- return f
- def get(self):
- return self.f.readline()
- def convert(reader):
- return reader.get().split(',')
- import unittest
- try:
- # Python3
- from unittest import mock
- except:
- # Python2
- import mock
- from utils import Reader, convert
- class ReaderTest(unittest.TestCase):
- @mock.patch('utils.open')
- @mock.patch('gzip.open')
- def test_gzip_open(self, mock_gzip, mock_open):
- mock_gzip.return_value = 'Mock Gzip'
- reader = Reader('test.csv.gz')
- mock_gzip.assert_called_with('test.csv.gz', 'rb')
- mock_open.assert_not_called()
- self.assertEqual(reader.f, 'Mock Gzip')
- @mock.patch('utils.open')
- @mock.patch('gzip.open')
- def test_builtins_open(self, mock_gzip, mock_open):
- mock_gzip.side_effect = Exception('Not this')
- mock_open.return_value = 'Open'
- reader = Reader('test.csv')
- mock_gzip.assert_called_with('test.csv', 'rb')
- mock_open.assert_called_with('test.csv', 'r')
- self.assertEqual(reader.f, 'Open')
- @mock.patch('utils.Reader.open')
- def test_get(self, mock_open):
- mock_open.return_value.readline.side_effect = [1, 2]
- reader = Reader('test.csv')
- self.assertEqual(reader.get(), 1)
- self.assertEqual(reader.get(), 2)
- with self.assertRaises(StopIteration):
- reader.get()
- class ConverterTest(unittest.TestCase):
- def test_convert(self):
- mock_reader = mock.MagicMock()
- mock_reader.get.return_value = '1,2,3'
- self.assertEqual(convert(mock_reader), ['1', '2', '3'])
- if __name__ == '__main__':
- unittest.main()
* 26.5. unittest.mock — mock object library
* 26.6. unittest.mock — getting started
* An Introduction to Mocking in Python
* Python Mock Tutorial
沒有留言:
張貼留言