Mocking(模仿)對Testing十分重要,它可以取代本來system會做的某些功能,通常用於:
- Network Request(太費時了)
- 一些複雜的Dependencies(避免需要準備太多東西/不想被其他module影響到)
- 修改當前時間(尤其是依賴
datetime.now()
的function)
各種mock
from django.test import mock
# 本來是 from unittest import mock
Mock
建立一個Mock
object來test:
m = Mock()
m.method()
m.method.assert_called() # OK
m.hahaha() # 甚麼名字也會自動變成valid method
m.hahaha.assert_called_once() # OK
最主要有幾種用法:
- spec
- side_effect
- return_value
spec
設定這個mock是甚麼:
mock = Mock(spec=3)
isinstance(mock, int) # True
def f(a, b, c): pass
mock = Mock(spec=f)
mock(1, 2, c=3)
mock.assert_called_with(1, 2, 3) # OK
mock.assert_called_with(a=1, b=2, c=3) # OK
side_effect
每次mock被call的時候都會run一次。
可用作三種東西:
Exception
mock = Mock(side_effect=KeyError('foo'))
mock() # KeyError
Function
side_effect = lambda value: value + 1
mock = Mock(side_effect=side_effect)
mock(1) # 2
mock(-5) # -4
Iterable
mock = Mock(side_effect=[1,2,3])
mock() # 1
mock() # 2
mock() # 3
mock() # StopIteration Error
return_value
最簡單的功能了,就是call的時候return一些固定的東西。
(有side_effect
的話,side_effect
優先)
mock = Mock(return_value=3)
mock() # 3
mock() # 3
MagicMock
MagicMock
是Mock
的subclass,通常會直接這個而不用Mock
,因為它支援了很多python的原生功能,例如__str__
,詳細可見這裡。
StackOverflow - Mock vs MagicMock
其他Mock
的詳細用法可參見官方文檔。
patch
替換module裡的任何東西,可用作decorator或with-block。
https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch
Patch一個class進去function argument
class SomeClass:
pass
@patch('__main__.SomeClass')
def function(normal_argument, mock_class):
print(normal_argument)
print(mock_class is SomeClass)
function(123)
# 123
# True
Patch一個class成為Mock object:
class Class:
def method(self):
pass
with patch('__main__.Class') as Mock:
instance = MockClass.return_value
instance.method.return_value = 'foo'
assert Class() is instance # OK
assert Class().method() == 'foo'
替換成new_callable
thing = object()
with patch('__main__.thing', new_callable=NonCallableMock) as mock_thing:
assert thing is mock_thing # OK
thing() # Error
return_value
from datetime import datetime
with patch('__main__.datetime.now', return_value=datetime.now().replace(hour=0, minute=0)):
datetime.now() # datetime(2018, xx, xx, 0, 0, x, xxxxxx)
side_effect
@mock.patch('requests.get', side_effect=mocked_requests_get)
實用例子
Mock network request
很好的例子:https://stackoverflow.com/a/28507806/6025730
Mock datetime
from django.test import TestCase, mock
from django.utils import timezone
from some_app.models import Restaurant
class SomeTestCase(TestCase):
def setUp(self):
Restaurant.objects.create(
name='Test1',
auto_open_at='18:00', # should auto open at 18:00
)
def test_auto_open_restaurants(self):
restaurant = Restaurant.active_objects.get(name='Test1')
self.assertEqual(restaurant.is_open, False) # OK: Not opened yet
with mock.patch(
'some_app.models.timezone.now', # which will be used in auto_open_restaurants()
return_value=timezone.now().replace(hour=18, minute=0) # Should consider timezone if django.settings.USE_TZ=true (e.g. 18:00 UTC+8 => 10:00 UTC)
):
auto_open_restaurants() # Should auto open here no matter when I run the test
restaurant = Restaurant.active_objects.get(name='Test1')
self.assertEqual(restaurant.is_open, True) # OK