unittest.mock --- شروع به کار¶
اضافه شده در نسخهی 3.3.
استفاده از ماک¶
متدهای وصلهکردن ماک¶
کاربردهای رایج برای اشیای Mock عبارتند از:
روشهای وصله کردن
ثبت فراخوانیهای متد روی اشیاء
ممکن است بخواهید یک متد از یک شیء را جایگزین کنید تا بررسی کنید که توسط بخش دیگری از سیستم با آرگومانهای صحیح فراخوانی میشود:
>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>
هنگامی که ماک ما استفاده شده باشد (در این مثال real.method)، این ماک متدها و ویژگیهایی دارد که به شما امکان میدهند دربارهی چگونگی استفاده از آن ادعاهایی مطرح کنید.
توجه
در بیشتر این مثالها، کلاسهای Mock و MagicMock قابل تعویض هستند. از آنجا که MagicMock کلاس توانمندتری است، گزینه معقولی برای استفاده بهصورت پیشفرض است.
پس از اینکه ماک فراخوانی شد، ویژگی called آن روی True تنظیم میشود. مهمتر اینکه، میتوانیم از متد assert_called_with() یا assert_called_once_with() برای بررسی اینکه با آرگومانهای صحیح فراخوانی شده است، استفاده کنیم.
این مثال آزمایش میکند که فراخوانی ProductionClass().method منجر به فراخوانی متد something میشود:
>>> class ProductionClass:
... def method(self):
... self.something(1, 2, 3)
... def something(self, a, b, c):
... pass
...
>>> real = ProductionClass()
>>> real.something = MagicMock()
>>> real.method()
>>> real.something.assert_called_once_with(1, 2, 3)
ماک برای فراخوانیهای متد روی یک شیء¶
در مثال قبلی، یک متد را بهطور مستقیم روی یک شیء وصله کردیم (patch) تا بررسی کنیم که بهدرستی فراخوانی شده است. یکی دیگر از کاربردهای رایج این است که یک شیء را به یک متد (یا بخشی از سیستم تحت آزمون) منتقل کنیم و سپس بررسی کنیم که از آن به روش صحیح استفاده میشود.
ProductionClass ساده زیر یک متد closer دارد. اگر این متد با یک شیء فراخوانی شود، close را روی آن فراخوانی میکند.
>>> class ProductionClass:
... def closer(self, something):
... something.close()
...
بنابراین برای آزمون آن باید شیءای با متد close ارسال کنیم و بررسی کنیم که بهدرستی فراخوانی شده است.
>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()
برای فراهم کردن متد 'close' در ماک خود نیازی به انجام هیچ کاری نیست. دسترسی به close آن را ایجاد میکند. بنابراین، اگر 'close' قبلاً فراخوانی نشده باشد، دسترسی به آن در آزمون آن را ایجاد میکند، اما assert_called_with() یک استثنای شکست را پرتاب میکند.
ماک کردن کلاسها¶
یک مورد استفاده رایج، ماک کردن کلاسهایی است که توسط کد تحت آزمون شما نمونهسازی میشوند. هنگامی که یک کلاس را وصله (patch) میکنید، آن کلاس با یک ماک جایگزین میشود. نمونهها با فراخوانی کلاس ایجاد میشوند. این بدان معناست که شما با بررسی مقدار بازگشتی کلاس ماکشده، به «نمونه ماک» دسترسی پیدا میکنید.
در مثال زیر، تابعی به نام some_function داریم که یک نمونه از Foo میسازد و متدی را روی آن فراخوانی میکند. فراخوانی patch() کلاس Foo را با یک ماک جایگزین میکند. نمونه Foo نتیجهی فراخوانی ماک است، بنابراین با تغییر return_value ماک پیکربندی میشود.
>>> def some_function():
... instance = module.Foo()
... return instance.method()
...
>>> with patch('module.Foo') as mock:
... instance = mock.return_value
... instance.method.return_value = 'the result'
... result = some_function()
... assert result == 'the result'
نامگذاری ماکهای شما¶
اختصاص نام به ماکهایتان میتواند مفید باشد. این نام در بازنمایی (repr) ماک نمایش داده میشود و میتواند هنگامی که ماک در پیامهای شکست آزمون ظاهر میشود، کمککننده باشد. نام همچنین به ویژگیها یا متدهای ماک نیز منتقل میشود:
>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>
پیگیری همه فراخوانیها¶
اغلب میخواهید بیش از یک فراخوانی به یک متد را پیگیری کنید. ویژگی mock_calls تمام فراخوانیها به ویژگیهای فرزند ماک - و همچنین به فرزندان آنها - را ثبت میکند.
>>> mock = MagicMock()
>>> mock.method()
<MagicMock name='mock.method()' id='...'>
>>> mock.attribute.method(10, x=53)
<MagicMock name='mock.attribute.method()' id='...'>
>>> mock.mock_calls
[call.method(), call.attribute.method(10, x=53)]
اگر درباره mock_calls ادعایی مطرح کنید و متدهای غیرمنتظرهای فراخوانی شده باشند، آن ادعا شکست خواهد خورد. این مفید است زیرا علاوه بر ادعای اینکه فراخوانیهای مورد انتظار شما انجام شدهاند، بررسی میکنید که آنها با ترتیب درست و بدون فراخوانیهای اضافی انجام شدهاند:
شما از شیء call برای ساخت فهرستهایی جهت مقایسه با mock_calls استفاده میکنید:
>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True
با این حال، پارامترهای فراخوانیهایی که ماک برمیگردانند ثبت نمیشوند، بدین معنا که نمیتوان فراخوانیهای تودرتویی را که در آنها پارامترهای استفادهشده برای ایجاد نیاکان اهمیت دارند پیگیری کرد:
>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True
تنظیم مقادیر بازگشتی و ویژگیها¶
تنظیم مقادیر بازگشتی در یک شیء ماک بسیار آسان است:
>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3
البته میتوانید همین کار را برای متدهای ماک نیز انجام دهید:
>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3
مقدار بازگشتی همچنین میتواند در سازنده تنظیم شود:
>>> mock = Mock(return_value=3)
>>> mock()
3
اگر نیاز به تنظیم یک ویژگی روی ماک خود دارید، فقط آن را انجام دهید:
>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3
گاهی میخواهید یک موقعیت پیچیدهتر را با ماک شبیهسازی کنید، برای مثال mock.connection.cursor().execute("SELECT 1"). اگر بخواهیم این فراخوانی یک فهرست برگرداند، باید نتیجهی فراخوانی تودرتو را پیکربندی کنیم.
میتوانیم از call برای ساختن مجموعهای از فراخوانیها در یک «فراخوانی زنجیرهای» مانند این استفاده کنیم تا تصدیق آن بعداً آسان باشد:
>>> mock = Mock()
>>> cursor = mock.connection.cursor.return_value
>>> cursor.execute.return_value = ['foo']
>>> mock.connection.cursor().execute("SELECT 1")
['foo']
>>> expected = call.connection.cursor().execute("SELECT 1").call_list()
>>> mock.mock_calls
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True
این فراخوانی .call_list() است که شیء فراخوانی ما را به فهرستی از فراخوانیها تبدیل میکند که نشاندهندهی فراخوانیهای زنجیرهای است.
پرتاب استثناها با ماکها¶
یکی از ویژگیهای مفید side_effect است. اگر این ویژگی را برابر با یک کلاس یا نمونهای از استثنا تنظیم کنید، آن استثنا هنگام فراخوانی ماک پرتاب میشود.
>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
...
Exception: Boom!
توابع دارای اثر جانبی و پیمایشپذیرها¶
همچنین میتوان side_effect را به یک تابع یا پیمایشپذیر تنظیم کرد. کاربرد side_effect بهعنوان یک پیمایشپذیر برای حالتی است که ماک شما قرار است چندین بار فراخوانی شود و میخواهید هر فراخوانی مقدار متفاوتی را برگرداند. هنگامی که side_effect را به یک پیمایشپذیر تنظیم میکنید، هر فراخوانی ماک مقدار بعدی را از پیمایشپذیر برمیگرداند:
>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6
برای موارد استفاده پیشرفتهتر، مانند تغییر پویای مقادیر بازگشتی بسته به اینکه ماک با چه آرگومانهایی فراخوانی میشود، side_effect میتواند یک تابع باشد. تابع با همان آرگومانهایی که ماک با آنها فراخوانی میشود، فراخوانی خواهد شد. هر مقداری که تابع برگرداند، همان مقداری است که فراخوانی برمیگرداند:
>>> vals = {(1, 2): 1, (2, 3): 2}
>>> def side_effect(*args):
... return vals[args]
...
>>> mock = MagicMock(side_effect=side_effect)
>>> mock(1, 2)
1
>>> mock(2, 3)
2
ماک کردن پیمایشگرهای ناهمگام¶
از پایتون 3.8، AsyncMock و MagicMock از ماک کردن پیمایشگرهای ناهمگام از طریق __aiter__ پشتیبانی میکنند. میتوان از ویژگی return_value در __aiter__ برای تنظیم مقادیر بازگشتی مورد استفاده در تکرار استفاده کرد.
>>> mock = MagicMock() # AsyncMock also works here
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
... return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]
ماک کردن مدیر زمینه ناهمگام¶
از پایتون 3.8، AsyncMock و MagicMock از ماک کردن مدیرهای زمینه ناهمگام از طریق __aenter__ و __aexit__ پشتیبانی میکنند. بهطور پیشفرض، __aenter__ و __aexit__ نمونههایی از AsyncMock هستند که یک تابع ناهمگام را برمیگردانند.
>>> class AsyncContextManager:
... async def __aenter__(self):
... return self
... async def __aexit__(self, exc_type, exc, tb):
... pass
...
>>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock also works here
>>> async def main():
... async with mock_instance as result:
... pass
...
>>> asyncio.run(main())
>>> mock_instance.__aenter__.assert_awaited_once()
>>> mock_instance.__aexit__.assert_awaited_once()
ایجاد یک ماک از یک شیء موجود¶
یکی از مشکلات استفادهی بیش از حد از ماک این است که آزمونهای شما را به پیادهسازی ماکهایتان وابسته میکند، نه به کد واقعیتان. فرض کنید کلاسی دارید که some_method را پیادهسازی میکند. در آزمونی برای کلاسی دیگر، یک ماک از این شیء فراهم میکنید که همچنین some_method را فراهم میکند. اگر بعداً کلاس اول را بازسازی (refactor) کنید، بهطوری که دیگر some_method نداشته باشد، آنگاه آزمونهای شما همچنان قبول میشوند، حتی اگر کدتان اکنون شکسته باشد!
Mock به شما امکان میدهد با استفاده از آرگومان کلیدواژهای spec، یک شیء را بهعنوان مشخصه برای ماک ارائه کنید. دسترسی به متدها / ویژگیهای ماک که در شیء مشخصه شما وجود ندارند، بلافاصله یک خطای ویژگی پرتاب میکند. اگر پیادهسازی مشخصه خود را تغییر دهید، آزمونهایی که از آن کلاس استفاده میکنند بلافاصله شروع به شکست میکنند، بدون آنکه مجبور باشید کلاس را در آن آزمونها نمونهسازی کنید.
>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'old_method'. Did you mean: 'class_method'?
استفاده از یک مشخصات همچنین امکان تطبیق هوشمندتر فراخوانیهای انجامشده به ماک را، صرفنظر از اینکه برخی پارامترها بهصورت آرگومانهای جایگاهی یا نامدار ارسال شده باشند، فراهم میکند:
>>> def f(a, b, c): pass
...
>>> mock = Mock(spec=f)
>>> mock(1, 2, 3)
<Mock name='mock()' id='140161580456576'>
>>> mock.assert_called_with(a=1, b=2, c=3)
اگر میخواهید این تطبیق هوشمندتر برای فراخوانیهای متد روی ماک نیز کار کند، میتوانید از auto-speccing استفاده کنید.
اگر شکل قویتری از مشخصات میخواهید که علاوه بر جلوگیری از تنظیم ویژگیهای دلخواه، از خواندن آنها نیز جلوگیری کند، میتوانید بهجای spec از spec_set استفاده کنید.
استفاده از side_effect برای برگرداندن محتوای هر پرونده¶
mock_open() برای وصله کردن (patch) متد open() استفاده میشود. میتوان از side_effect برای برگرداندن یک شیء Mock جدید بهازای هر فراخوانی استفاده کرد. این را میتوان برای برگرداندن محتوای متفاوت بهازای هر پرونده ذخیرهشده در یک دیکشنری به کار برد:
DEFAULT = "default"
data_dict = {"file1": "data1",
"file2": "data2"}
def open_side_effect(name):
return mock_open(read_data=data_dict.get(name, DEFAULT))()
with patch("builtins.open", side_effect=open_side_effect):
with open("file1") as file1:
assert file1.read() == "data1"
with open("file2") as file2:
assert file2.read() == "data2"
with open("file3") as file2:
assert file2.read() == "default"
دکوراتورهای patch¶
توجه
در استفاده از patch()، مهم است که اشیاء را در فضای نامی وصله (patch) کنید که در آن جستجو میشوند. این کار معمولاً ساده است، اما برای یک راهنمای سریع، محل وصلهکردن را بخوانید.
نیاز رایج در آزمونها، وصله کردن یک ویژگی کلاس یا یک ویژگی ماژول است؛ برای مثال وصله کردن یک شیء توکار یا وصله کردن یک کلاس در یک ماژول برای آزمون اینکه از آن نمونهسازی میشود. ماژولها و کلاسها عملاً سراسری هستند، بنابراین وصله کردن روی آنها باید پس از آزمون لغو شود؛ در غیر این صورت وصله به آزمونهای دیگر باقی میماند و مشکلاتی ایجاد میکند که تشخیص آنها دشوار است.
ماک برای این منظور سه دکوراتور مناسب فراهم میکند: patch()، patch.object() و patch.dict(). patch یک رشته واحد به شکل package.module.Class.attribute میگیرد تا ویژگیای را که در حال وصلهکردن آن هستید مشخص کند. همچنین بهصورت اختیاری مقداری را میگیرد که میخواهید آن ویژگی (یا کلاس یا هر چیز دیگر) با آن جایگزین شود. 'patch.object' یک شیء و نام ویژگیای را که میخواهید وصله (patch) شود میگیرد، بههمراه مقداری اختیاری برای وصلهکردن (patch) آن.
patch.object:
>>> original = SomeClass.attribute
>>> @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test():
... assert SomeClass.attribute == sentinel.attribute
...
>>> test()
>>> assert SomeClass.attribute == original
>>> @patch('package.module.attribute', sentinel.attribute)
... def test():
... from package.module import attribute
... assert attribute is sentinel.attribute
...
>>> test()
اگر در حال وصله کردن یک ماژول (از جمله builtins) هستید، از patch() به جای patch.object() استفاده کنید:
>>> mock = MagicMock(return_value=sentinel.file_handle)
>>> with patch('builtins.open', mock):
... handle = open('filename', 'r')
...
>>> mock.assert_called_with('filename', 'r')
>>> assert handle == sentinel.file_handle, "incorrect file handle returned"
نام ماژول میتواند در صورت نیاز بهصورت «نقطهدار» و در قالب package.module باشد:
>>> @patch('package.module.ClassName.attribute', sentinel.attribute)
... def test():
... from package.module import ClassName
... assert ClassName.attribute == sentinel.attribute
...
>>> test()
یک الگوی مناسب این است که در واقع دکوراتور را روی خود متدهای آزمون اعمال کنید:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test_something(self):
... self.assertEqual(SomeClass.attribute, sentinel.attribute)
...
>>> original = SomeClass.attribute
>>> MyTest('test_something').test_something()
>>> assert SomeClass.attribute == original
اگر میخواهید با یک ماک وصله (patch) کنید، میتوانید از patch() تنها با یک آرگومان استفاده کنید (یا patch.object() با دو آرگومان). ماک برای شما ایجاد میشود و به تابع / متد آزمون داده میشود:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'static_method')
... def test_something(self, mock_method):
... SomeClass.static_method()
... mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()
میتوانید با استفاده از این الگو، چندین دکوراتور patch را روی هم انباشته کنید:
>>> class MyTest(unittest.TestCase):
... @patch('package.module.ClassName1')
... @patch('package.module.ClassName2')
... def test_something(self, MockClass2, MockClass1):
... self.assertIs(package.module.ClassName1, MockClass1)
... self.assertIs(package.module.ClassName2, MockClass2)
...
>>> MyTest('test_something').test_something()
هنگامی که دکوراتورهای patch را تودرتو میکنید، ماکها به همان ترتیبی که دکوراتورها اعمال شدهاند به تابع آراستهشده ارسال میشوند (ترتیب معمول Python برای اعمال دکوراتورها). این یعنی از پایین به بالا، بنابراین در مثال بالا ماک مربوط به test_module.ClassName2 ابتدا ارسال میشود.
همچنین patch.dict() برای تنظیم مقادیر در یک دیکشنری فقط در طول یک محدوده و بازگرداندن دیکشنری به حالت اولیهاش هنگامی که آزمون پایان مییابد وجود دارد:
>>> foo = {'key': 'value'}
>>> original = foo.copy()
>>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
... assert foo == {'newkey': 'newvalue'}
...
>>> assert foo == original
patch، patch.object و patch.dict همگی میتوانند بهعنوان مدیر زمینه استفاده شوند.
هرجا که از patch() برای ایجاد یک ماک برای خود استفاده میکنید، میتوانید با استفاده از شکل «as» دستور with، ارجاعی به ماک دریافت کنید:
>>> class ProductionClass:
... def method(self):
... pass
...
>>> with patch.object(ProductionClass, 'method') as mock_method:
... mock_method.return_value = None
... real = ProductionClass()
... real.method(1, 2, 3)
...
>>> mock_method.assert_called_with(1, 2, 3)
بهعنوان جایگزین، میتوان از patch، patch.object و patch.dict بهعنوان دکوراتورهای کلاس استفاده کرد. هنگامی که به این شکل استفاده شوند، مانند آن است که دکوراتور بهصورت جداگانه روی هر متدی که نام آن با "test" شروع میشود اعمال شود.
مثالهای بیشتر¶
در اینجا چند مثال دیگر برای برخی حالتهای کمی پیشرفتهتر آورده شده است.
ماک کردن فراخوانیهای زنجیرهای¶
ماک کردن فراخوانیهای زنجیرهای با استفاده از ماک در واقع ساده است، بهشرط آنکه ویژگی return_value را درک کنید. هنگامی که یک ماک برای نخستین بار فراخوانی میشود، یا return_value آن را پیش از آنکه فراخوانی شده باشد دریافت میکنید، یک Mock جدید ایجاد میشود.
این بدان معناست که شما میتوانید با پرسوجو از ماک return_value ببینید شیء برگرداندهشده از فراخوانی یک شیء ماکشده چگونه استفاده شده است:
>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)
از اینجا تنها یک گام ساده تا پیکربندی و سپس مطرح کردن ادعاها دربارهی فراخوانیهای زنجیرهای باقی است. البته یک جایگزین دیگر این است که از همان ابتدا کد خود را به شکلی آزمونپذیرتر بنویسید…
بنابراین، فرض کنید کدی داریم که کمی شبیه این است:
>>> class Something:
... def __init__(self):
... self.backend = BackendProvider()
... def method(self):
... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
... # more code
با فرض اینکه BackendProvider از قبل بهخوبی آزمون شده است، چگونه method() را آزمون کنیم؟ بهطور مشخص، میخواهیم آزمون کنیم که بخش کد # more code از شیء پاسخ بهشکل صحیح استفاده میکند.
از آنجا که این زنجیرهی فراخوانیها از یک ویژگی نمونه انجام میشود، میتوانیم ویژگی backend را روی یک نمونه از Something مانکیوصله (monkey patch) کنیم. در این مورد خاص، فقط به مقدار بازگشتی از آخرین فراخوانی start_call علاقهمندیم، بنابراین پیکربندی چندانی برای انجام نداریم. فرض کنیم شیءای که برمیگرداند «شبیه پرونده» است، بنابراین اطمینان حاصل میکنیم که شیء پاسخ ما از تابع توکار open() بهعنوان spec خود استفاده میکند.
برای انجام این کار، یک نمونه ماک بهعنوان بکاند ماک خود ایجاد میکنیم و یک شیء پاسخ ماک برای آن میسازیم. برای تنظیم پاسخ بهعنوان مقدار بازگشتی برای آن start_call نهایی، میتوانیم اینگونه عمل کنیم:
mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response
میتوانیم این کار را بهشکلی کمی بهتر با استفاده از متد configure_mock() انجام دهیم تا مقدار بازگشتی را مستقیماً برای ما تنظیم کند:
>>> something = Something()
>>> mock_response = Mock(spec=open)
>>> mock_backend = Mock()
>>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response}
>>> mock_backend.configure_mock(**config)
با این موارد، «بکاند ماک» را درجا مانکیوصله (monkey patch) میکنیم و میتوانیم فراخوانی واقعی را انجام دهیم:
>>> something.backend = mock_backend
>>> something.method()
با استفاده از mock_calls میتوانیم فراخوانی زنجیرهای را با یک ادعای واحد بررسی کنیم. یک فراخوانی زنجیرهای چندین فراخوانی در یک خط کد است، بنابراین چندین ورودی در mock_calls وجود خواهد داشت. میتوانیم از call.call_list() استفاده کنیم تا این فهرست از فراخوانیها را برای ما ایجاد کند:
>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
>>> call_list = chained.call_list()
>>> assert mock_backend.mock_calls == call_list
ماک جزئی¶
برای برخی از آزمونها، ممکن است بخواهید فراخوانی datetime.date.today() را با ماک جایگزین کنید تا یک تاریخ مشخص را برگرداند، اما نمیخواهید کد تحت آزمون را از ایجاد اشیای جدید date بازدارید. متأسفانه datetime.date به زبان C نوشته شده است، بنابراین نمیتوانید بهسادگی متد استاتیک datetime.date.today() را با تغییر در رانتایم (monkey-patch) جایگزین کنید.
در عوض، میتوانید عملاً کلاس date را با یک ماک بپوشانید، در حالی که فراخوانیهای سازنده به کلاس واقعی عبور داده میشوند (و نمونههای واقعی برگردانده میشوند).
در اینجا از دکوراتور patch برای ماک کردن کلاس date در ماژول تحت آزمون استفاده میشود. سپس ویژگی side_effect در کلاس date ماکشده به یک تابع لامبدا تنظیم میشود که یک date واقعی برمیگرداند. هنگامی که کلاس date ماکشده فراخوانی شود، یک date واقعی توسط side_effect ایجاد و برگردانده خواهد شد.
>>> import datetime as dt
>>> with patch('mymodule.date') as mock_date:
... mock_date.today.return_value = dt.date(2010, 10, 8)
... mock_date.side_effect = lambda *args, **kw: dt.date(*args, **kw)
...
... assert mymodule.date.today() == dt.date(2010, 10, 8)
... assert mymodule.date(2009, 6, 8) == dt.date(2009, 6, 8)
توجه داشته باشید که ما datetime.date را بهصورت سراسری وصله (patch) نمیکنیم، بلکه date را در ماژولی که از آن استفاده میکند وصله میکنیم. به محل وصله مراجعه کنید.
هنگامی که date.today() فراخوانی شود، یک تاریخ شناختهشده برگردانده میشود، اما فراخوانیهای سازنده date(...) همچنان تاریخهای عادی را برمیگردانند. بدون این امکان، ممکن است مجبور شوید نتیجه مورد انتظار را با دقیقاً همان الگوریتمی که کد تحت آزمون استفاده میکند محاسبه کنید، که یک ضدالگوی کلاسیک در آزمون است.
فراخوانیهای سازندهی date در ویژگیهای mock_date (call_count و موارد مرتبط) ثبت میشوند که ممکن است برای آزمونهای شما نیز مفید باشند.
راه جایگزینی برای سروکار داشتن با ماک کردن تاریخها، یا سایر کلاسهای توکار، در این مطلب وبگزارشی بحث شده است.
ماککردن یک متد تولیدگر¶
یک تولیدگر پایتون، تابع یا متدی است که از دستور yield استفاده میکند تا هنگامی که بر روی آن تکرار میشود، مجموعهای از مقادیر را برگرداند [1].
یک متد / تابع تولیدگر فراخوانی میشود تا شیء تولیدگر را برگرداند. این شیء تولیدگر است که سپس مورد تکرار قرار میگیرد. متد پروتکل برای تکرار، __iter__() است، بنابراین میتوانیم آن را با استفاده از یک MagicMock ماک کنیم.
در اینجا یک کلاس نمونه با یک متد "iter" که بهصورت یک تولیدگر پیادهسازی شده است:
>>> class Foo:
... def iter(self):
... for i in [1, 2, 3]:
... yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]
چگونه میتوانیم این کلاس را ماک کنیم، و بهویژه متد "iter" آن را؟
برای پیکربندی مقدارهایی که از پیمایش بازگردانده میشوند (که در فراخوانی list بهصورت ضمنی انجام میشود)، باید شیئی را که فراخوانی foo.iter() بازمیگرداند پیکربندی کنیم.
>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]
اعمال وصله (patch) یکسان به همه متدهای آزمون¶
اگر بخواهید چندین وصله (patch) برای چندین متد آزمون برقرار باشند، راه بدیهی این است که دکوراتورهای وصله را بر هر متد اعمال کنید. این کار ممکن است تکرار غیرضروری به نظر برسد. در عوض، میتوانید از patch() (در تمام شکلهای مختلفش) بهعنوان دکوراتور کلاس استفاده کنید. این کار وصلهها را بر همه متدهای آزمون کلاس اعمال میکند. یک متد آزمون بهصورت متدی که نام آن با test شروع میشود، شناسایی میشود:
>>> @patch('mymodule.SomeClass')
... class MyTest(unittest.TestCase):
...
... def test_one(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def test_two(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def not_a_test(self):
... return 'something'
...
>>> MyTest('test_one').test_one()
>>> MyTest('test_two').test_two()
>>> MyTest('test_two').not_a_test()
'something'
یک راه جایگزین برای مدیریت وصلهها استفاده از متدهای patch: start و stop است. این موارد به شما امکان میدهند وصلهکردن را به متدهای setUp و tearDown خود منتقل کنید.
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... self.patcher = patch('mymodule.foo')
... self.mock_foo = self.patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
... def tearDown(self):
... self.patcher.stop()
...
>>> MyTest('test_foo').run()
اگر از این روش استفاده میکنید، باید اطمینان حاصل کنید که وصلهکردن با فراخوانی stop «واگرد» میشود. این کار ممکن است پیچیدهتر از آنچه تصور میکنید باشد، زیرا اگر در setUp استثنایی پرتاب شود، tearDown فراخوانی نمیشود. unittest.TestCase.addCleanup() این کار را آسانتر میکند:
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... patcher = patch('mymodule.foo')
... self.addCleanup(patcher.stop)
... self.mock_foo = patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
>>> MyTest('test_foo').run()
ماک کردن متدهای مقیدنشده¶
گاهی یک آزمون نیاز به وصله کردن (patch) یک متد مقیدنشده (unbound method) دارد، که به معنای وصله کردن متد روی کلاس است، نه روی نمونه. برای اینکه بتوانید ادعا کنید کدام شیءها این متد ویژه را فراخوانی میکردند، باید self را بهعنوان اولین آرگومان ارسال کنید. مسئله این است که نمیتوانید این متد مقیدنشده را با یک ماک وصله کنید، زیرا اگر یک متد مقیدنشده را با یک ماک جایگزین کنید، هنگام بازیابی از نمونه به یک متد مقید (bound method) تبدیل نمیشود، و بنابراین self به آن ارسال نمیشود. راهحل این است که در عوض، متد مقیدنشده را با یک تابع واقعی وصله کنید. دکوراتور patch() وصله کردن متدها با یک ماک را چنان ساده میکند که مجبور شدن به ایجاد یک تابع واقعی به یک دردسر تبدیل میشود.
اگر autospec=True را به patch بدهید، عملیات patch را با یک شیء تابع واقعی انجام میدهد. این شیء تابع همان امضای تابعی را دارد که جایگزین آن میشود، اما در پشت صحنه کار را به یک ماک محول میکند. شما همچنان ماک خود را دقیقاً به همان شیوهی قبل بهصورت خودکار ایجادشده دریافت میکنید. با این حال، معنای آن این است که اگر از آن برای جایگزینی (patch out) یک متد غیرمقید در یک کلاس استفاده کنید، تابع ماکشده در صورت واکشی از یک نمونه به یک متد مقید تبدیل میشود. در این حالت self بهعنوان اولین آرگومان به آن گذرانده میشود، که دقیقاً همان چیزی است که نیاز بود:
>>> class Foo:
... def foo(self):
... pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
... mock_foo.return_value = 'foo'
... foo = Foo()
... foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)
اگر از autospec=True استفاده نکنیم، آنگاه در عوض، متد غیرمقید با یک نمونهی Mock جایگزین میشود و با self فراخوانی نمیشود.
بررسی فراخوانیهای متعدد با ماک¶
ماک API مناسبی برای ایجاد ادعاهایی دربارهی نحوهی استفاده از اشیای ماک شما دارد.
>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')
اگر ماک شما فقط یک بار فراخوانی میشود، میتوانید از متد assert_called_once_with() استفاده کنید که همچنین تأیید میکند call_count برابر با ۱ است.
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
>>> mock.foo_bar()
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
Traceback (most recent call last):
...
AssertionError: Expected 'foo_bar' to be called once. Called 2 times.
Calls: [call('baz', spam='eggs'), call()].
هر دو assert_called_with و assert_called_once_with ادعاهایی دربارهی آخرین فراخوانی مطرح میکنند. اگر ماک شما قرار باشد چندین بار فراخوانی شود و بخواهید دربارهی تمام آن فراخوانیها ادعاهایی مطرح کنید، میتوانید از call_args_list استفاده کنید:
>>> mock = Mock(return_value=None)
>>> mock(1, 2, 3)
>>> mock(4, 5, 6)
>>> mock()
>>> mock.call_args_list
[call(1, 2, 3), call(4, 5, 6), call()]
شیء کمکی call مطرح کردن ادعاهایی درباره این فراخوانیها را آسان میکند. شما میتوانید فهرستی از فراخوانیهای مورد انتظار بسازید و آن را با call_args_list مقایسه کنید. این بهطرز چشمگیری شبیه به repr مربوط به call_args_list است:
>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True
کنار آمدن با آرگومانهای تغییرپذیر¶
موقعیت دیگری که نادر است، اما میتواند شما را به دردسر بیندازد، زمانی است که ماک شما با آرگومانهای تغییرپذیر فراخوانی میشود. call_args و call_args_list ارجاعهایی به آرگومانها را ذخیره میکنند. اگر آرگومانها توسط کد تحت آزمون تغییر کنند، دیگر نمیتوانید دربارهی اینکه مقادیر هنگام فراخوانی ماک چه بودهاند ادعا کنید.
در ادامه چند نمونه کد آمده است که مشکل را نشان میدهد. فرض کنید توابع زیر در 'mymodule' تعریف شدهاند:
def frob(val):
pass
def grob(val):
"First frob and then clear val"
frob(val)
val.clear()
وقتی سعی میکنیم آزمون کنیم که grob، frob را با آرگومان صحیح فراخوانی میکند، ببینید چه اتفاقی میافتد:
>>> with patch('mymodule.frob') as mock_frob:
... val = {6}
... mymodule.grob(val)
...
>>> val
set()
>>> mock_frob.assert_called_with({6})
Traceback (most recent call last):
...
AssertionError: Expected: (({6},), {})
Called with: ((set(),), {})
یکی از امکانها این است که ماک آرگومانهایی را که به آن میدهید کپی کند. در این صورت، اگر ادعاهایی مطرح کنید که برای برابری به هویت شیء متکی هستند، ممکن است مشکلاتی پیش بیاید.
در اینجا راهحلی ارائه میشود که از قابلیت side_effect استفاده میکند. اگر یک تابع side_effect برای یک ماک ارائه کنید، آنگاه side_effect با همان آرگومانهایی که ماک دریافت میکند فراخوانی میشود. این کار به ما امکان میدهد که آرگومانها را کپی کنیم و برای ادعاهای بعدی ذخیره کنیم. در این مثال، من از یک ماک دیگر برای ذخیره آرگومانها استفاده میکنم تا بتوانم از متدهای ماک برای انجام ادعا استفاده کنم. باز هم یک تابع کمکی این تنظیم را برای من انجام میدهد.
>>> from copy import deepcopy
>>> from unittest.mock import Mock, patch, DEFAULT
>>> def copy_call_args(mock):
... new_mock = Mock()
... def side_effect(*args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... new_mock(*args, **kwargs)
... return DEFAULT
... mock.side_effect = side_effect
... return new_mock
...
>>> with patch('mymodule.frob') as mock_frob:
... new_mock = copy_call_args(mock_frob)
... val = {6}
... mymodule.grob(val)
...
>>> new_mock.assert_called_with({6})
>>> new_mock.call_args
call({6})
copy_call_args با ماکی که قرار است فراخوانی شود، فراخوانی میشود. ماک جدیدی برمیگرداند که روی آن ادعا انجام میدهیم. تابع side_effect یک کپی از آرگومانها میسازد و new_mock ما را با آن کپی فراخوانی میکند.
توجه
اگر ماک شما قرار است تنها یک بار استفاده شود، راه سادهتری برای بررسی آرگومانها در لحظهای که فراخوانی میشود وجود دارد. میتوانید بهسادگی بررسی را درون یک تابع side_effect انجام دهید.
>>> def side_effect(arg):
... assert arg == {6}
...
>>> mock = Mock(side_effect=side_effect)
>>> mock({6})
>>> mock(set())
Traceback (most recent call last):
...
AssertionError
یک رویکرد جایگزین، ایجاد زیرکلاسی از Mock یا MagicMock است که آرگومانها را (با استفاده از copy.deepcopy()) کپی میکند. در ادامه یک پیادهسازی نمونه آمده است:
>>> from copy import deepcopy
>>> class CopyingMock(MagicMock):
... def __call__(self, /, *args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... return super().__call__(*args, **kwargs)
...
>>> c = CopyingMock(return_value=None)
>>> arg = set()
>>> c(arg)
>>> arg.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(arg)
Traceback (most recent call last):
...
AssertionError: expected call not found.
Expected: mock({1})
Actual: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>
هنگامی که از Mock یا MagicMock زیرکلاس میسازید، تمام ویژگیهایی که بهصورت پویا ایجاد میشوند و return_value بهطور خودکار از زیرکلاس شما استفاده خواهند کرد. این بدان معناست که تمام فرزندان یک CopyingMock نیز دارای نوع CopyingMock خواهند بود.
تودرتو کردن وصلهها (patches)¶
استفاده از patch بهعنوان یک مدیر زمینه خوب است، اما اگر چندین patch انجام دهید، ممکن است با دستورهای with تودرتویی مواجه شوید که بیشتر و بیشتر به سمت راست تورفتگی پیدا میکنند:
>>> class MyTest(unittest.TestCase):
...
... def test_foo(self):
... with patch('mymodule.Foo') as mock_foo:
... with patch('mymodule.Bar') as mock_bar:
... with patch('mymodule.Spam') as mock_spam:
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').test_foo()
>>> assert mymodule.Foo is original
با توابع cleanup در unittest و متدهای patch: start و stop میتوانیم بدون تورفتگی تودرتو به همان اثر برسیم. یک متد کمکی ساده، create_patch، وصله (patch) را اعمال میکند و ماک ایجادشده را برای ما برمیگرداند:
>>> class MyTest(unittest.TestCase):
...
... def create_patch(self, name):
... patcher = patch(name)
... thing = patcher.start()
... self.addCleanup(patcher.stop)
... return thing
...
... def test_foo(self):
... mock_foo = self.create_patch('mymodule.Foo')
... mock_bar = self.create_patch('mymodule.Bar')
... mock_spam = self.create_patch('mymodule.Spam')
...
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').run()
>>> assert mymodule.Foo is original
ماک کردن یک دیکشنری با MagicMock¶
شاید بخواهید یک دیکشنری یا شیء ظرف دیگر را ماک کنید، بهطوری که تمام دسترسیها به آن ثبت شود و همچنان مانند یک دیکشنری رفتار کند.
میتوانیم این کار را با MagicMock انجام دهیم، که مانند یک دیکشنری رفتار خواهد کرد، و با استفاده از side_effect دسترسی به دیکشنری را به یک دیکشنری واقعی زیرین که تحت کنترل ما است واگذار کنیم.
وقتی متدهای __getitem__() و __setitem__() از MagicMock ما فراخوانی میشوند (دسترسی عادی به دیکشنری)، side_effect با کلید فراخوانی میشود (و در مورد __setitem__ با مقدار نیز). همچنین میتوانیم آنچه را که بازگشت داده میشود کنترل کنیم.
پس از استفاده از MagicMock، میتوانیم از ویژگیهایی مانند call_args_list برای تصدیق چگونگی استفاده از دیکشنری استفاده کنیم:
>>> my_dict = {'a': 1, 'b': 2, 'c': 3}
>>> def getitem(name):
... return my_dict[name]
...
>>> def setitem(name, val):
... my_dict[name] = val
...
>>> mock = MagicMock()
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
توجه
یک جایگزین برای استفاده از MagicMock این است که از Mock استفاده کنید و فقط متدهای جادویی را که بهطور مشخص میخواهید، فراهم کنید:
>>> mock = Mock()
>>> mock.__getitem__ = Mock(side_effect=getitem)
>>> mock.__setitem__ = Mock(side_effect=setitem)
سومین گزینه این است که از MagicMock استفاده کنید، اما dict را بهعنوان آرگومان spec (یا spec_set) ارسال کنید تا MagicMock ایجادشده فقط متدهای جادویی دیکشنری را در دسترس داشته باشد:
>>> mock = MagicMock(spec_set=dict)
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
با وجود این توابع اثر جانبی، mock مانند یک دیکشنری معمولی رفتار میکند، اما دسترسیها را ثبت میکند. حتی اگر سعی کنید به کلیدی که وجود ندارد دسترسی پیدا کنید، یک KeyError پرتاب میکند.
>>> mock['a']
1
>>> mock['c']
3
>>> mock['d']
Traceback (most recent call last):
...
KeyError: 'd'
>>> mock['b'] = 'fish'
>>> mock['d'] = 'eggs'
>>> mock['b']
'fish'
>>> mock['d']
'eggs'
پس از استفاده از آن، میتوانید با متدها و ویژگیهای معمول ماک، ادعاهایی را درباره دسترسی مطرح کنید:
>>> mock.__getitem__.call_args_list
[call('a'), call('c'), call('d'), call('b'), call('d')]
>>> mock.__setitem__.call_args_list
[call('b', 'fish'), call('d', 'eggs')]
>>> my_dict
{'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}
زیرکلاسهای ماک و ویژگیهای آنها¶
دلایل مختلفی وجود دارد که ممکن است بخواهید از Mock زیرکلاس بسازید. یکی از این دلایل ممکن است افزودن متدهای کمکی باشد. در اینجا یک مثال ساده آورده شده است:
>>> class MyMock(MagicMock):
... def has_been_called(self):
... return self.called
...
>>> mymock = MyMock(return_value=None)
>>> mymock
<MyMock id='...'>
>>> mymock.has_been_called()
False
>>> mymock()
>>> mymock.has_been_called()
True
رفتار استاندارد برای نمونههای Mock این است که ویژگیها و ماکهای مقدار بازگشتی، از همان نوع ماکی هستند که از روی آن به آنها دسترسی پیدا میشود. این موضوع تضمین میکند که ویژگیهای Mock از نوع Mocks و ویژگیهای MagicMock از نوع MagicMocks باشند [2]. بنابراین اگر برای افزودن متدهای کمکی زیرکلاسسازی میکنید، آنها روی ویژگیها و ماک مقدار بازگشتی نمونههای زیرکلاس شما نیز در دسترس خواهند بود.
>>> mymock.foo
<MyMock name='mock.foo' id='...'>
>>> mymock.foo.has_been_called()
False
>>> mymock.foo()
<MyMock name='mock.foo()' id='...'>
>>> mymock.foo.has_been_called()
True
گاهی اوقات این موضوع نامطلوب است. برای مثال، یک کاربر در حال ساخت زیرکلاسی از ماک برای ایجاد یک آداپتور Twisted است. اعمال این موضوع بر روی ویژگیها نیز در واقع باعث خطا میشود.
Mock (در همه انواعش) از متدی به نام _get_child_mock برای ایجاد این «زیرماکها» برای ویژگیها و مقادیر بازگشتی استفاده میکند. شما میتوانید با بازنویسی این متد، از استفادهی زیرکلاس خود برای ویژگیها جلوگیری کنید. امضای آن به این صورت است که آرگومانهای کلیدواژهای دلخواه (**kwargs) میگیرد و سپس به سازندهی ماک منتقل میشوند:
>>> class Subclass(MagicMock):
... def _get_child_mock(self, /, **kwargs):
... return MagicMock(**kwargs)
...
>>> mymock = Subclass()
>>> mymock.foo
<MagicMock name='mock.foo' id='...'>
>>> assert isinstance(mymock, Subclass)
>>> assert not isinstance(mymock.foo, Subclass)
>>> assert not isinstance(mymock(), Subclass)
استثنایی بر این قانون، ماکهای غیرفراخوانیپذیر هستند. ویژگیها از گونهی فراخوانیپذیر استفاده میکنند، زیرا در غیر این صورت ماکهای غیرفراخوانیپذیر نمیتوانستند متدهای فراخوانیپذیر داشته باشند.
ماک کردن ایمپورتها با patch.dict¶
یکی از موقعیتهایی که ماک کردن میتواند دشوار باشد، هنگامی است که یک ایمپورت محلی درون یک تابع دارید. ماک کردن آنها سختتر است، زیرا از یک شیء در فضای نام ماژول استفاده نمیکنند که بتوانیم آن را وصله کنیم (patch out).
بهطور کلی، باید از ایمپورتهای محلی اجتناب کرد. گاهی اوقات ایمپورت محلی برای جلوگیری از وابستگیهای چرخهای انجام میشود، که برای آنها معمولاً راه بسیار بهتری برای حل مشکل وجود دارد (کد را بازسازی کنید)، یا برای جلوگیری از «هزینههای اولیه» با بهتأخیر انداختن ایمپورت. این مسئله را میتوان به روشهای بهتری نسبت به یک ایمپورت محلی بیقیدوشرط حل کرد (ماژول را بهعنوان یک ویژگی کلاس یا ماژول ذخیره کنید و ایمپورت را فقط در اولین استفاده انجام دهید).
گذشته از این، راهی برای استفاده از mock جهت تأثیرگذاری بر نتایج ایمپورت وجود دارد. ایمپورت کردن یک شیء را از دیکشنری sys.modules بازیابی میکند. توجه داشته باشید که این کار یک شیء را بازیابی میکند، که لازم نیست یک ماژول باشد. ایمپورت کردن یک ماژول برای اولین بار منجر به قرار گرفتن یک شیء ماژول در sys.modules میشود، بنابراین معمولاً وقتی چیزی را ایمپورت میکنید، یک ماژول دریافت میکنید. با این حال، لزوماً اینطور نیست.
این بدان معناست که میتوانید از patch.dict() استفاده کنید تا یک ماک را بهطور موقت در sys.modules قرار دهید. هر ایمپورتی در حالی که این وصله (patch) فعال است، ماک را دریافت خواهد کرد. هنگامی که وصله کامل شود (تابع دکوریتشده خارج شود، بدنهی دستور with کامل شود یا patcher.stop() فراخوانی شود)، هر آنچه پیشتر آنجا بود بهصورت امن بازگردانده خواهد شد.
در اینجا مثالی آمده است که ماژول 'fooble' را ماک میکند.
>>> import sys
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... import fooble
... fooble.blob()
...
<Mock name='mock.blob()' id='...'>
>>> assert 'fooble' not in sys.modules
>>> mock.blob.assert_called_once_with()
همانطور که میبینید import fooble موفق میشود، اما هنگام خروج هیچ 'fooble' در sys.modules باقی نمیماند.
این برای قالب from module import name نیز کار میکند:
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... from fooble import blob
... blob.blip()
...
<Mock name='mock.blob.blip()' id='...'>
>>> mock.blob.blip.assert_called_once_with()
با کمی کار بیشتر، میتوانید ایمپورت بستهها را نیز ماک کنید:
>>> mock = Mock()
>>> modules = {'package': mock, 'package.module': mock.module}
>>> with patch.dict('sys.modules', modules):
... from package.module import fooble
... fooble()
...
<Mock name='mock.module.fooble()' id='...'>
>>> mock.module.fooble.assert_called_once_with()
پیگیری ترتیب فراخوانیها و ادعاهای فراخوانی مختصرتر¶
کلاس Mock به شما امکان میدهد ترتیب فراخوانیهای متد روی اشیای ماک خود را از طریق ویژگی method_calls پیگیری کنید. این امکان به شما اجازه نمیدهد ترتیب فراخوانیها بین اشیای ماک جداگانه را پیگیری کنید، اما میتوانیم از mock_calls برای دستیابی به همان اثر استفاده کنیم.
از آنجا که ماکها فراخوانیهای ماکهای فرزند را در mock_calls پیگیری میکنند و دسترسی به یک ویژگی دلخواه از یک ماک، یک ماک فرزند ایجاد میکند، میتوانیم ماکهای جداگانهی خود را از یک ماک والد ایجاد کنیم. فراخوانیهای آن ماکهای فرزند سپس همگی بهترتیب در mock_calls ماک والد ثبت میشوند:
>>> manager = Mock()
>>> mock_foo = manager.foo
>>> mock_bar = manager.bar
>>> mock_foo.something()
<Mock name='mock.foo.something()' id='...'>
>>> mock_bar.other.thing()
<Mock name='mock.bar.other.thing()' id='...'>
>>> manager.mock_calls
[call.foo.something(), call.bar.other.thing()]
سپس میتوانیم دربارهی فراخوانیها، از جمله ترتیب آنها، با مقایسه با ویژگی mock_calls در ماک مدیر ادعا کنیم:
>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True
اگر patch ماکهای شما را ایجاد و جایگذاری میکند، میتوانید آنها را با استفاده از متد attach_mock() به یک ماک مدیر متصل کنید. پس از اتصال، فراخوانیها در mock_calls مدیر ثبت خواهند شد.
>>> manager = MagicMock()
>>> with patch('mymodule.Class1') as MockClass1:
... with patch('mymodule.Class2') as MockClass2:
... manager.attach_mock(MockClass1, 'MockClass1')
... manager.attach_mock(MockClass2, 'MockClass2')
... MockClass1().foo()
... MockClass2().bar()
<MagicMock name='mock.MockClass1().foo()' id='...'>
<MagicMock name='mock.MockClass2().bar()' id='...'>
>>> manager.mock_calls
[call.MockClass1(),
call.MockClass1().foo(),
call.MockClass2(),
call.MockClass2().bar()]
اگر فراخوانیهای زیادی انجام شده باشد، اما شما فقط به دنباله خاصی از آنها علاقهمند باشید، یک جایگزین این است که از متد assert_has_calls() استفاده کنید. این متد فهرستی از فراخوانیها (ساختهشده با شیء call) را دریافت میکند. اگر آن دنباله از فراخوانیها در mock_calls وجود داشته باشد، ادعا موفق میشود.
>>> m = MagicMock()
>>> m().foo().bar().baz()
<MagicMock name='mock().foo().bar().baz()' id='...'>
>>> m.one().two().three()
<MagicMock name='mock.one().two().three()' id='...'>
>>> calls = call.one().two().three().call_list()
>>> m.assert_has_calls(calls)
با وجود اینکه فراخوانی زنجیرهای m.one().two().three() تنها فراخوانیهایی نیست که به ماک انجام شدهاند، ادعا (assert) همچنان موفق میشود.
گاهی ممکن است یک ماک چندین فراخوانی دریافت کرده باشد، و شما فقط بخواهید دربارهی برخی از آن فراخوانیها ادعا کنید. حتی ممکن است ترتیب برای شما مهم نباشد. در این حالت میتوانید any_order=True را به assert_has_calls ارسال کنید:
>>> m = MagicMock()
>>> m(1), m.two(2, 3), m.seven(7), m.fifty('50')
(...)
>>> calls = [call.fifty('50'), call(1), call.seven(7)]
>>> m.assert_has_calls(calls, any_order=True)
تطبیق پیچیدهتر آرگومانها¶
با استفاده از همان مفهوم پایهای ANY، میتوانیم تطبیقدهندهها (matchers) را پیادهسازی کنیم تا ادعاهای پیچیدهتری روی اشیایی که بهعنوان آرگومان برای ماکها استفاده میشوند، انجام دهیم.
فرض کنید انتظار داریم یک شیء به یک ماک ارسال شود؛ این شیء بهطور پیشفرض بر اساس هویت شیء، برابر مقایسه میشود (که این حالت پیشفرض پایتون برای کلاسهای تعریفشده توسط کاربر است). برای استفاده از assert_called_with() باید دقیقاً همان شیء را ارسال کنیم. اگر فقط به برخی از ویژگیهای این شیء علاقهمند باشیم، میتوانیم یک تطبیقدهنده ایجاد کنیم که این ویژگیها را برای ما بررسی کند.
در این مثال میتوانید ببینید که چگونه یک فراخوانی «استاندارد» از assert_called_with کافی نیست:
>>> class Foo:
... def __init__(self, a, b):
... self.a, self.b = a, b
...
>>> mock = Mock(return_value=None)
>>> mock(Foo(1, 2))
>>> mock.assert_called_with(Foo(1, 2))
Traceback (most recent call last):
...
AssertionError: expected call not found.
Expected: mock(<__main__.Foo object at 0x...>)
Actual: mock(<__main__.Foo object at 0x...>)
یک تابع مقایسه برای کلاس Foo ما ممکن است چیزی شبیه به این باشد:
>>> def compare(self, other):
... if not type(self) == type(other):
... return False
... if self.a != other.a:
... return False
... if self.b != other.b:
... return False
... return True
...
و یک شیء تطبیقدهنده (matcher) که بتواند از توابع مقایسهای مانند این برای عملیات برابری خود استفاده کند، چیزی شبیه به این خواهد بود:
>>> class Matcher:
... def __init__(self, compare, some_obj):
... self.compare = compare
... self.some_obj = some_obj
... def __eq__(self, other):
... return self.compare(self.some_obj, other)
...
با ترکیب همه این موارد:
>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)
Matcher با تابع مقایسهی ما و شیء Foo که میخواهیم با آن مقایسه کنیم، نمونهسازی میشود. در assert_called_with، متد برابری Matcher فراخوانی میشود؛ این متد شیءای را که ماک با آن فراخوانی شده است با شیءای که تطبیقدهندهی خود را با آن ایجاد کردهایم، مقایسه میکند. اگر این دو مطابقت داشته باشند، assert_called_with موفق میشود و اگر نداشته باشند، یک AssertionError پرتاب میشود:
>>> match_wrong = Matcher(compare, Foo(3, 4))
>>> mock.assert_called_with(match_wrong)
Traceback (most recent call last):
...
AssertionError: Expected: ((<Matcher object at 0x...>,), {})
Called with: ((<Foo object at 0x...>,), {})
با کمی تغییر میتوانید کاری کنید که تابع مقایسه مستقیماً AssertionError را پرتاب کند و پیام شکست مفیدتری ارائه دهد.
از نسخه 1.5، کتابخانه آزمون پایتون PyHamcrest قابلیت مشابهی را، که ممکن است در اینجا مفید باشد، در قالب تطبیقدهنده برابری (equality matcher) خود (hamcrest.library.integration.match_equality) ارائه میدهد.