การยืนยันการเรียกไปยังเมธอด mock อย่างต่อเนื่อง


175

จำลองมีประโยชน์assert_called_with()วิธี อย่างไรก็ตามเท่าที่ฉันเข้าใจการตรวจสอบเพียงโทรครั้งสุดท้ายเพื่อวิธีการ
หากฉันมีรหัสที่เรียกใช้วิธีการเยาะเย้ย 3 ครั้งติดต่อกันแต่ละครั้งด้วยพารามิเตอร์ที่แตกต่างกันฉันจะยืนยันการโทร 3 ครั้งนี้ด้วยพารามิเตอร์เฉพาะได้อย่างไร

คำตอบ:


179

assert_has_calls เป็นอีกวิธีหนึ่งในการแก้ไขปัญหานี้

จากเอกสาร:

assert_has_calls (การโทร any_order = False)

ยืนยันการจำลองถูกเรียกด้วยการโทรที่ระบุ รายการ mock_calls ถูกตรวจสอบการโทร

ถ้า any_order เป็นเท็จ (ค่าเริ่มต้น) การโทรจะต้องต่อเนื่องกัน อาจมีการโทรเพิ่มเติมก่อนหรือหลังการโทรที่ระบุ

ถ้า any_order เป็น True การโทรสามารถอยู่ในลำดับใดก็ได้ แต่จะต้องปรากฏใน mock_calls

ตัวอย่าง:

>>> from unittest.mock import call, Mock
>>> mock = Mock(return_value=None)
>>> mock(1)
>>> mock(2)
>>> mock(3)
>>> mock(4)
>>> calls = [call(2), call(3)]
>>> mock.assert_has_calls(calls)
>>> calls = [call(4), call(2), call(3)]
>>> mock.assert_has_calls(calls, any_order=True)

ที่มา: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls


9
บิตแปลกที่พวกเขาเลือกที่จะเพิ่มใหม่ "เรียกว่า" ชนิดที่พวกเขายังจะได้ใช้เพียงแค่รายการหรือ tuple ที่ ...
jaapz

@jaapz มันคลาสย่อยtuple: ช่วยให้isinstance(mock.call(1), tuple) Trueพวกเขายังเพิ่มวิธีการและคุณลักษณะบางอย่าง
jpmc26

13
รุ่นแรก ๆ ของ Mock ใช้ tuple ธรรมดา แต่มันกลับกลายเป็นว่าใช้ไม่ได้ การเรียกใช้ฟังก์ชันแต่ละครั้งจะรับ tuple เป็น (args, kwargs) ดังนั้นเพื่อตรวจสอบว่า "foo (123)" ถูกเรียกอย่างถูกต้องคุณต้อง "ยืนยัน mock.call_args == ((123,), {})" ซึ่งเป็น คำเปรียบเทียบกับ "โทร (123)"
Jonathan Hartley

คุณจะทำอย่างไรในแต่ละครั้งของการโทรที่คุณคาดหวังว่าจะได้รับผลตอบแทนที่แตกต่างกัน?
CodeWithPride

2
@CodeWithPride ดูเหมือนว่าจะมีงานมากขึ้นside_effect
Pigueiras

108

โดยปกติฉันไม่สนใจคำสั่งของการโทรเท่านั้นที่เกิดขึ้น ในกรณีที่ผมรวมกับการยืนยันเกี่ยวกับassert_any_callcall_count

>>> import mock
>>> m = mock.Mock()
>>> m(1)
<Mock name='mock()' id='37578160'>
>>> m(2)
<Mock name='mock()' id='37578160'>
>>> m(3)
<Mock name='mock()' id='37578160'>
>>> m.assert_any_call(1)
>>> m.assert_any_call(2)
>>> m.assert_any_call(3)
>>> assert 3 == m.call_count
>>> m.assert_any_call(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "[python path]\lib\site-packages\mock.py", line 891, in assert_any_call
    '%s call not found' % expected_string
AssertionError: mock(4) call not found

ฉันพบว่าการทำเช่นนี้เพื่อให้อ่านและเข้าใจได้ง่ายกว่ารายการโทรจำนวนมากที่ส่งเป็นวิธีการเดียว

หากคุณใส่ใจในการสั่งซื้อหรือคุณคาดหวังว่าจะมีสายที่เหมือนกันหลายสายassert_has_callsอาจจะเหมาะสมกว่า

แก้ไข

ตั้งแต่ฉันโพสต์คำตอบนี้ฉันคิดใหม่เกี่ยวกับวิธีการทดสอบโดยทั่วไป ฉันคิดว่ามันคุ้มค่าที่จะกล่าวถึงว่าหากการทดสอบของคุณซับซ้อนนี้คุณอาจทำการทดสอบไม่เหมาะสมหรือมีปัญหาในการออกแบบ Mocks ถูกออกแบบมาสำหรับการทดสอบการสื่อสารระหว่างวัตถุในการออกแบบเชิงวัตถุ หากการออกแบบของคุณไม่ได้คัดค้าน (เช่นในขั้นตอนหรือการทำงานมากกว่า) การเยาะเย้ยอาจไม่เหมาะสมทั้งหมด นอกจากนี้คุณยังอาจใช้วิธีการมากเกินไปหรือคุณอาจทดสอบรายละเอียดภายในที่ยังไม่ได้ถอดออก ฉันพัฒนากลยุทธ์ที่กล่าวถึงในวิธีการนี้เมื่อรหัสของฉันไม่ได้มุ่งเน้นวัตถุมากนักและฉันเชื่อว่าฉันกำลังทดสอบรายละเอียดภายในที่จะไม่ได้รับการปลดจากตำแหน่งที่ดีที่สุด


@ jpmc26 คุณช่วยอธิบายเพิ่มเติมเกี่ยวกับการแก้ไขของคุณได้ไหม? คุณหมายถึงอะไรโดย 'ซ้ายที่ดีที่สุดที่ยังไม่ได้ตรวจสอบ' คุณจะทดสอบอีกวิธีถ้ามีการโทรภายในวิธีการ
otgw

@memo บ่อยครั้งจะเป็นการดีกว่าที่จะให้เรียกวิธีการจริง หากวิธีการอื่นถูกทำลายอาจทำให้การทดสอบหยุดชะงัก แต่ค่าของการหลีกเลี่ยงนั้นน้อยกว่าค่าของการทดสอบที่ง่ายกว่าและบำรุงรักษาได้มากกว่า เวลาที่ดีที่สุดในการเยาะเย้ยคือเมื่อการเรียกภายนอกไปยังวิธีอื่นคือสิ่งที่คุณต้องการทดสอบ (โดยปกติหมายถึงผลลัพธ์บางประเภทผ่านเข้าไปแล้วและรหัสที่ทดสอบไม่ส่งคืนผลลัพธ์) หรือวิธีอื่น มีการอ้างอิงภายนอก (ฐานข้อมูลเว็บไซต์) ที่คุณต้องการกำจัด (โดยทางเทคนิคกรณีสุดท้ายเป็นต้นขั้วมากกว่าและฉันลังเลที่จะยืนยันในเรื่องนี้)
jpmc26

@ jpmc26 การเยาะเย้ยมีประโยชน์เมื่อคุณต้องการหลีกเลี่ยงการพึ่งพาการฉีดหรือวิธีอื่น ๆ ในการเลือกกลยุทธ์รันไทม์ เช่นเดียวกับที่คุณกล่าวถึงการทดสอบตรรกะภายในของวิธีการโดยไม่ต้องเรียกใช้บริการจากภายนอกและที่สำคัญกว่านั้นโดยไม่ต้องรับรู้ถึงสภาพแวดล้อม (ไม่ต้องมีรหัสที่ดีdo() if TEST_ENV=='prod' else dont()) จะสามารถทำได้อย่างง่ายดาย ผลข้างเคียงของสิ่งนี้คือการรักษาการทดสอบต่อรุ่น (พูดว่าการเปลี่ยนรหัสระหว่าง google search api v1 และ v2 รหัสของคุณจะทดสอบเวอร์ชัน 1 ไม่ว่าจะเกิดอะไรขึ้น)
Daniel Dubovski

@DanielDubovski การทดสอบส่วนใหญ่ของคุณควรเป็นอินพุต / เอาท์พุตตาม มันไม่ได้เป็นไปได้เสมอไป แต่ถ้ามันเป็นไปไม่ได้เกือบตลอดเวลาคุณอาจมีปัญหาในการออกแบบ เมื่อคุณต้องการค่าที่ส่งคืนซึ่งโดยปกติมาจากรหัสอื่นและคุณต้องการลดการพึ่งพาส่วนใหญ่จะทำส่วนย่อย Mocks จำเป็นเฉพาะเมื่อคุณต้องการตรวจสอบว่ามีการเรียกฟังก์ชันการแก้ไขบางสถานะ (อาจไม่มีค่าส่งคืน) (ความแตกต่างระหว่างเยาะเย้ยและต้นขั้วคือการที่คุณไม่ยืนยันในการโทรด้วยต้นขั้ว) การใช้ mocks ซึ่งต้นขั้วจะทำให้การทดสอบของคุณบำรุงรักษาน้อยลง
jpmc26

@ jpmc26 ไม่ได้เรียกใช้บริการภายนอกประเภทใด แน่นอนคุณสามารถ refactor ออกรหัสที่สร้างข้อความที่จะส่งและทดสอบแทนการยืนยัน params โทร แต่ IMHO มันเหมือนกันมาก คุณจะแนะนำให้ออกแบบการเรียก API ภายนอกได้อย่างไรฉันยอมรับว่าการเยาะเย้ยนั้นควรลดให้น้อยที่สุดทุกคนพูดว่าคุณไม่สามารถทำการทดสอบข้อมูลที่คุณส่งไปยังบริการภายนอกเพื่อให้แน่ใจว่าตรรกะทำงานได้ตามที่คาดไว้
Daniel Dubovski

46

คุณสามารถใช้Mock.call_args_listแอ็ตทริบิวต์เพื่อเปรียบเทียบพารามิเตอร์กับการเรียกใช้เมธอดก่อนหน้า ร่วมกับMock.call_countคุณสมบัติควรให้การควบคุมเต็มรูปแบบ


9
assert_has_calls ()?
bavaza

5
assert_has_callsตรวจสอบเฉพาะว่ามีการโทรที่คาดไว้หรือไม่ แต่หากเป็นเพียงการโทรนั้น
blueyed

17

ฉันต้องดูครั้งนี้ซ้ำแล้วซ้ำอีกดังนั้นนี่คือคำตอบของฉัน


การยืนยันการเรียกใช้หลายวิธีบนวัตถุที่แตกต่างของคลาสเดียวกัน

สมมติว่าเรามีชั้นเรียนหนัก (ซึ่งเราต้องการเยาะเย้ย):

In [1]: class HeavyDuty(object):
   ...:     def __init__(self):
   ...:         import time
   ...:         time.sleep(2)  # <- Spends a lot of time here
   ...:     
   ...:     def do_work(self, arg1, arg2):
   ...:         print("Called with %r and %r" % (arg1, arg2))
   ...:  

นี่คือรหัสบางส่วนที่ใช้สองอินสแตนซ์ของ HeavyDutyคลาส:

In [2]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(13, 17)
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(23, 29)
   ...:    


ตอนนี้นี่คือกรณีทดสอบสำหรับheavy_workฟังก์ชั่น:

In [3]: from unittest.mock import patch, call
   ...: def test_heavy_work():
   ...:     expected_calls = [call.do_work(13, 17),call.do_work(23, 29)]
   ...:     
   ...:     with patch('__main__.HeavyDuty') as MockHeavyDuty:
   ...:         heavy_work()
   ...:         MockHeavyDuty.return_value.assert_has_calls(expected_calls)
   ...:  

เราจะเยาะเย้ยชั้นเรียนกับHeavyDuty MockHeavyDutyวิธีการที่จะยืนยันการโทรมาจากทุกHeavyDutyตัวอย่างเช่นเราต้องดูแทนMockHeavyDuty.return_value.assert_has_calls MockHeavyDuty.assert_has_callsนอกจากนี้ในรายการexpected_callsเราต้องระบุชื่อวิธีการที่เราสนใจในการยืนยันการโทร ดังนั้นรายการของเราจะทำจากการโทรไปเมื่อเทียบกับเพียงcall.do_workcall

การออกกำลังกายกรณีทดสอบแสดงให้เราเห็นว่ามันประสบความสำเร็จ:

In [4]: print(test_heavy_work())
None


หากเราปรับเปลี่ยนheavy_workฟังก์ชั่นการทดสอบล้มเหลวและสร้างข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์:

In [5]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(113, 117)  # <- call args are different
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(123, 129)  # <- call args are different
   ...:     

In [6]: print(test_heavy_work())
---------------------------------------------------------------------------
(traceback omitted for clarity)

AssertionError: Calls not found.
Expected: [call.do_work(13, 17), call.do_work(23, 29)]
Actual: [call.do_work(113, 117), call.do_work(123, 129)]


การยืนยันการเรียกหลายฟังก์ชั่น

นี่คือตัวอย่างที่แสดงวิธีเยาะเย้ยการเรียกหลายฟังก์ชั่น:

In [7]: def work_function(arg1, arg2):
   ...:     print("Called with args %r and %r" % (arg1, arg2))

In [8]: from unittest.mock import patch, call
   ...: def test_work_function():
   ...:     expected_calls = [call(13, 17), call(23, 29)]    
   ...:     with patch('__main__.work_function') as mock_work_function:
   ...:         work_function(13, 17)
   ...:         work_function(23, 29)
   ...:         mock_work_function.assert_has_calls(expected_calls)
   ...:    

In [9]: print(test_work_function())
None


มีความแตกต่างที่สำคัญสองประการ คนแรกคือว่าเมื่อเยาะเย้ยการตั้งค่าฟังก์ชั่นที่เราคาดว่าสายของเราโดยใช้แทนการใช้call call.some_methodคนที่สองคือการที่เราเรียกassert_has_callsบนแทนในmock_work_functionmock_work_function.return_value

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.