ฟังก์ชั่นหลามเยาะเย้ยตามข้อโต้แย้งอินพุต


150

เราใช้Mockกับ python มาระยะหนึ่งแล้ว

ตอนนี้เรามีสถานการณ์ที่เราต้องการจำลองฟังก์ชั่น

def foo(self, my_param):
    #do something here, assign something to my_result
    return my_result

โดยปกติวิธีการเยาะเย้ยนี้จะเป็น (สมมติว่าเป็นส่วนหนึ่งของวัตถุ foo)

self.foo = MagicMock(return_value="mocked!")

แม้ว่าฉันจะโทรหา foo () สองครั้งฉันก็สามารถใช้ได้

self.foo = MagicMock(side_effect=["mocked once", "mocked twice!"])

ตอนนี้ฉันกำลังเผชิญสถานการณ์ที่ฉันต้องการคืนค่าคงที่เมื่อพารามิเตอร์อินพุตมีค่าเฉพาะ ถ้าสมมุติว่า "my_param" เท่ากับ "อะไรบางอย่าง" แล้วฉันต้องการคืน "my_cool_mock"

ดูเหมือนว่าจะมีให้ในmockito สำหรับไพ ธ อน

when(dummy).foo("something").thenReturn("my_cool_mock")

ฉันค้นหาวิธีการที่จะประสบความสำเร็จกับ Mock โดยไม่ประสบความสำเร็จได้อย่างไร

ความคิดใด ๆ


2
อาจเป็นคำตอบนี้จะช่วย - stackoverflow.com/a/7665754/234606
naiquevin

@naiquevin มันแก้ปัญหาเพื่อนได้อย่างสมบูรณ์แบบขอบคุณ!
Juan Antonio Gomez Moriano

ฉันไม่รู้ว่าคุณสามารถใช้ Mocktio กับ Python +1 ได้!
Ben

ถ้าใช้โครงการของคุณ pytest monkeypatchสำหรับวัตถุประสงค์ดังกล่าวคุณอาจต้องการที่จะใช้ประโยชน์จาก Monkeypatch เป็นมากกว่าสำหรับ "แทนที่ฟังก์ชั่นนี้เพื่อประโยชน์ในการทดสอบ" ในขณะที่ Mock คือสิ่งที่คุณใช้เมื่อคุณต้องการตรวจสอบmock_callsหรือยืนยันเกี่ยวกับสิ่งที่มันถูกเรียกด้วยเป็นต้น มีสถานที่สำหรับทั้งสองและฉันมักจะใช้ทั้งสองในเวลาที่แตกต่างกันในไฟล์ทดสอบที่กำหนด
Driftcatcher

คำตอบ:


187

ถ้าside_effectเป็นฟังก์ชั่นแล้วฟังก์ชันใดก็ตามที่ส่งคืนคือสิ่งที่เรียกไปยังการจำลองจำลอง side_effectฟังก์ชั่นที่เรียกว่ามีข้อโต้แย้งเช่นเดียวกับการจำลอง วิธีนี้ช่วยให้คุณสามารถเปลี่ยนค่าตอบแทนของการโทรแบบไดนามิกโดยอิงตามอินพุต:

>>> def side_effect(value):
...     return value + 1
...
>>> m = MagicMock(side_effect=side_effect)
>>> m(1)
2
>>> m(2)
3
>>> m.mock_calls
[call(1), call(2)]

http://www.voidspace.org.uk/python/mock/mock.html#calling


25
เพื่อให้คำตอบง่ายขึ้นคุณสามารถเปลี่ยนชื่อฟังก์ชัน side_effect เป็นอย่างอื่นได้หรือไม่? (ฉันรู้ว่าฉันรู้ว่ามันสวยเรียบง่าย แต่ช่วยเพิ่มความสามารถในการอ่านความจริงที่ว่าชื่อฟังก์ชันและชื่อพารามิเตอร์จะแตกต่างกัน :)
อันโตนิโอฆ Gomez Moriano

6
@JuanAntonioGomezMoriano ฉันทำได้ แต่ในกรณีนี้ฉันแค่อ้างถึงเอกสารโดยตรงฉันจึงไม่กล้าแก้ไขใบเสนอราคาถ้ามันไม่ได้แตกหักโดยเฉพาะ
แอมเบอร์

และจะอวดรู้ทุกปีต่อมา แต่side effectเป็นคำที่ถูกต้อง: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh

7
@Ish พวกเขาไม่ได้บ่นเกี่ยวกับชื่อของCallableMixin.side_effectแต่ฟังก์ชั่นแยกที่กำหนดไว้ในตัวอย่างมีชื่อเดียวกัน
OrangeDog

48

ตามที่ระบุไว้ที่วัตถุ Python Mock ด้วยวิธีการที่เรียกว่าหลายครั้ง

ทางออกคือการเขียน side_effect ของฉันเอง

def my_side_effect(*args, **kwargs):
    if args[0] == 42:
        return "Called with 42"
    elif args[0] == 43:
        return "Called with 43"
    elif kwargs['foo'] == 7:
        return "Foo is seven"

mockobj.mockmethod.side_effect = my_side_effect

นั่นเป็นกลอุบาย


2
นี่เองที่ทำให้มันชัดเจนสำหรับผมกว่าคำตอบที่เลือกเพื่อขอบคุณสำหรับการตอบคำถามของคุณเอง :)
Luca Bezerra

15

ผลข้างเคียงรับฟังก์ชั่น (ซึ่งอาจเป็นฟังก์ชั่นแลมบ์ดา ) ดังนั้นสำหรับกรณีง่ายๆคุณอาจใช้:

m = MagicMock(side_effect=(lambda x: x+1))

4

ฉันสิ้นสุดที่นี่มองหา " วิธีการเยาะเย้ยฟังก์ชั่นตามข้อโต้แย้ง input " และในที่สุดฉันก็แก้ไขสิ่งนี้สร้างฟังก์ชั่น aux ง่าย:

def mock_responses(responses, default_response=None):
  return lambda input: responses[input] if input in responses else default_response

ขณะนี้:

my_mock.foo.side_effect = mock_responses(
  {
    'x': 42, 
    'y': [1,2,3]
  })
my_mock.goo.side_effect = mock_responses(
  {
    'hello': 'world'
  }, 
  default_response='hi')
...

my_mock.foo('x') # => 42
my_mock.foo('y') # => [1,2,3]
my_mock.foo('unknown') # => None

my_mock.goo('hello') # => 'world'
my_mock.goo('ey') # => 'hi'

หวังว่านี่จะช่วยใครซักคน!


2

คุณสามารถใช้partialจากfunctoolsหากคุณต้องการใช้ฟังก์ชั่นที่รับพารามิเตอร์ แต่ฟังก์ชั่นที่คุณกำลังเยาะเย้ยไม่ทำงาน เช่นนี้

def mock_year(year):
    return datetime.datetime(year, 11, 28, tzinfo=timezone.utc)
@patch('django.utils.timezone.now', side_effect=partial(mock_year, year=2020))

สิ่งนี้จะส่งกลับ callable ที่ไม่ยอมรับพารามิเตอร์ (เช่นเขตเวลาของ Django (หิมะ)) แต่ฟังก์ชัน mock_year ของฉันทำ


ขอบคุณสำหรับการแก้ปัญหาที่สง่างามนี้ ฉันชอบที่จะเพิ่มว่าถ้าฟังก์ชั่นดั้งเดิมของคุณมีพารามิเตอร์เพิ่มเติมพวกเขาจะต้องถูกเรียกด้วยคำหลักในรหัสการผลิตของคุณหรือวิธีการนี้จะไม่ทำงาน คุณได้รับข้อผิดพลาด: got multiple values for argument.
Erik Kalkoken

1

เพียงเพื่อแสดงวิธีอื่นในการทำ:

def mock_isdir(path):
    return path in ['/var/log', '/var/log/apache2', '/var/log/tomcat']

with mock.patch('os.path.isdir') as os_path_isdir:
    os_path_isdir.side_effect = mock_isdir

1

คุณยังสามารถใช้ @mock.patch.object :

สมมติว่าโมดูลmy_module.pyใช้pandasเพื่ออ่านจากฐานข้อมูลและเราต้องการทดสอบโมดูลนี้โดยpd.read_sql_tableวิธีการเยาะเย้ย(ซึ่งต้องใช้table_nameเป็นอาร์กิวเมนต์)

สิ่งที่คุณสามารถทำได้คือการสร้าง (ภายในการทดสอบของคุณ) db_mockวิธีการที่ส่งกลับวัตถุที่แตกต่างกันขึ้นอยู่กับการโต้แย้งให้:

def db_mock(**kwargs):
    if kwargs['table_name'] == 'table_1':
        # return some DataFrame
    elif kwargs['table_name'] == 'table_2':
        # return some other DataFrame

ในฟังก์ชั่นการทดสอบของคุณคุณจะต้อง:

import my_module as my_module_imported

@mock.patch.object(my_module_imported.pd, "read_sql_table", new_callable=lambda: db_mock)
def test_my_module(mock_read_sql_table):
    # You can now test any methods from `my_module`, e.g. `foo` and any call this 
    # method does to `read_sql_table` will be mocked by `db_mock`, e.g.
    ret = my_module_imported.foo(table_name='table_1')
    # `ret` is some DataFrame returned by `db_mock`

1

หากคุณ"ต้องการส่งคืนค่าคงที่เมื่อพารามิเตอร์อินพุตมีค่าเฉพาะ"บางทีคุณอาจไม่ต้องการเยาะเย้ยและสามารถใช้เมธอดdictพร้อมด้วยget:

foo = {'input1': 'value1', 'input2': 'value2'}.get

foo('input1')  # value1
foo('input2')  # value2

วิธีนี้ใช้ได้ดีเมื่อเอาต์พุตปลอมของคุณคือการแม็พอินพุต เมื่อมันเป็นฟังก์ชั่นของอินพุตฉันขอแนะนำให้ใช้side_effectตามคำตอบของแอมเบอร์

นอกจากนี้คุณยังสามารถใช้การรวมกันของทั้งสองถ้าคุณต้องการที่จะรักษาMockความสามารถในการ s ( assert_called_once, call_countฯลฯ ):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get

มันฉลาดมาก
emyller

-6

ฉันรู้ว่ามันค่อนข้างคำถามเก่าอาจช่วยปรับปรุงโดยใช้ python lamdba

self.some_service.foo.side_effect = lambda *args:"Called with 42" \
            if args[0] == 42 \
            else "Called with 42" if args[0] == 43 \
            else "Called with 43" if args[0] == 43 \
            else "Called with 45" if args[0] == 45 \
            else "Called with 49" if args[0] == 49 else None
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.