一些常用的mock示例
先簡單定義個類,方便舉例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Person: def __init__(self): self.__age = 10 def get_fullname(self, first_name, last_name): return first_name + ' ' + last_name def get_age(self): return self.__age @staticmethod def get_class_name(): return Person.__name__ |
這個類裡有兩個成員方法,一個有引數,一個無引數。還有一個靜態方法
mock成員方法
1. 使用Mock類,返回固定值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class PersonTest(TestCase): def test_should_get_age(self): p = Person() # 不mock時,get_age應該返回10 self.assertEqual(p.get_age(), 10) # mock掉get_age方法,讓它返回20 p.get_age = Mock(return_value=20) self.assertEqual(p.get_age(), 20) def test_should_get_fullname(self): p = Person() # mock掉get_fullname,讓它返回'James Harden' p.get_fullname = Mock(return_value='James Harden') self.assertEqual(p.get_fullname(), 'James Harden') |
2. 校驗引數個數,再返回固定值
上面的例子你也許已經注意到了,呼叫p.get_fullname時沒有給任何的引數,但是依然可以工作。
如果想校驗引數需要用create_autospec模組方法替代Mock類。
1 2 3 4 5 6 7 8 9 10 11 |
class PersonTest(TestCase): def test_should_get_fullname(self): p = Person() p.get_fullname = create_autospec(p.get_fullname, return_value='James Harden') # 隨便給兩個引數,依然會返回mock的值 self.assertEqual(p.get_fullname('1', '2'), 'James Harden') # 如果引數個數不對,會報錯TypeError: missing a required argument: 'last_name' p.get_fullname('1') |
3. 使用side_effect, 依次返回指定值
1 2 3 4 5 6 7 8 9 |
class PersonTest(TestCase): def test_should_get_age(self): p = Person() p.get_age = Mock(side_effect=[10, 11, 12]) self.assertEqual(p.get_age(), 10) self.assertEqual(p.get_age(), 11) self.assertEqual(p.get_age(), 12) |
4. 根據引數不同,返回不同的值
1 2 3 4 5 6 7 8 9 |
class PersonTest(TestCase): def test_should_get_fullname(self): p = Person() values = {('James', 'Harden'): 'James Harden', ('Tracy', 'Grady'): 'Tracy Grady'} p.get_fullname = Mock(side_effect=lambda x, y: values[(x, y)]) self.assertEqual(p.get_fullname('James', 'Harden'), 'James Harden') self.assertEqual(p.get_fullname('Tracy', 'Grady'), 'Tracy Grady') |
5. 丟擲異常
1 2 3 4 5 6 7 |
class PersonTest(TestCase): def test_should_raise_exception(self): p = Person() p.get_age = Mock(side_effect=TypeError('integer type')) # 只要調就會丟擲異常 self.assertRaises(TypeError, p.get_age) |
6. 檢驗是否呼叫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class PersonTest(TestCase): def test_should_validate_method_calling(self): p.get_fullname = Mock(return_value='James Harden') # 沒呼叫過 p.get_fullname.assert_not_called() # Python 3.5 p.get_fullname('1', '2') # 呼叫過任意次數 p.get_fullname.assert_called() # Python 3.6 # 只呼叫過一次, 不管引數 p.get_fullname.assert_called_once() # Python 3.6 # 只呼叫過一次,並且符合指定的引數 p.get_fullname.assert_called_once_with('1', '2') p.get_fullname('3', '4') # 只要呼叫過即可,必須指定引數 p.get_fullname.assert_any_call('1', '2') # 重置mock,重置之後相當於沒有呼叫過 p.get_fullname.reset_mock() p.get_fullname.assert_not_called() # Mock物件裡除了return_value, side_effect屬性外, # called表示是否呼叫過,call_count可以返回撥用的次數 self.assertEqual(p.get_fullname.called, False) self.assertEqual(p.get_fullname.call_count, 0) p.get_fullname('1', '2') p.get_fullname('3', '4') self.assertEqual(p.get_fullname.called, True) self.assertEqual(p.get_fullname.call_count, 2) |
mock靜態方法
靜態方法和模組方法需要使用patch來mock。
1. 在測試方法引數中得到Mock物件
1 2 3 4 5 6 7 |
class PersonTest(TestCase): # 以字串的形式列出靜態方法的路徑,在測試的引數裡會自動得到一個Mock物件 @patch('your.package.module.Person.get_class_name') def test_should_get_class_name(self, mock_get_class_name): mock_get_class_name.return_value = 'Guy' self.assertEqual(Person.get_class_name(), 'Guy') |
2. 在patch中設定Mock物件
1 2 3 4 5 6 7 |
class PersonTest(TestCase): mock_get_class_name = Mock(return_value='Guy') # 在patch中給出定義好的Mock的物件,好處是定義好的物件可以複用 @patch('your.package.module.Person.get_class_name', mock_get_class_name) def test_should_get_class_name(self): self.assertEqual(Person.get_class_name(), 'Guy') |
3. 使用patch.object
1 2 3 4 5 6 7 |
class PersonTest(TestCase): mock_get_class_name = Mock(return_value='Guy') # 使用patch.object來mock,好處是Person類不是以字串形式給出的 @patch.object(Person, 'get_class_name', mock_get_class_name) def test_should_get_class_name(self, ): self.assertEqual(Person.get_class_name(), 'Guy') |
4. 使用with控制作用域
1 2 3 4 5 6 7 8 |
class PersonTest(TestCase): # 作用域之外,依然返回真實值 def test_should_get_class_name(self, ): mock_get_class_name = Mock(return_value='Guy') with patch('your.package.module.Person.get_class_name', mock_get_class_name): self.assertEqual(Person.get_class_name(), 'Guy') self.assertEqual(Person.get_class_name(), 'Person') |
mock鏈式呼叫
在django裡,我們經常需要mock資料庫,而訪問資料庫時經常是鏈式呼叫,看個例子。
1 2 |
def get_person(name): return Person.objects.filter(name=name).order_by('age') |
有個模組方法,返回資料庫中所有指定name的人員,並按age排序
mock掉整個資料庫訪問
1 2 3 4 5 6 |
@patch('your.package.module.Person.objects.filter') def test_should_get_person(self, mock_filter): # 先得到一個filter的Mock物件,再在return_value中設定一個Mock物件,此時不需要自己再建立 mock_filter.return_value.order_by.return_value = None self.assertIsNone(get_person()) |