Chức năng python dựa trên các đối số đầu vào


150

Chúng tôi đã sử dụng Mock cho python một thời gian.

Bây giờ, chúng ta có một tình huống trong đó chúng ta muốn mô phỏng một hàm

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

Thông thường, cách để chế giễu điều này sẽ là (giả sử foo là một phần của một đối tượng)

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

Thậm chí, nếu tôi gọi foo () một vài lần tôi có thể sử dụng

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

Bây giờ, tôi đang phải đối mặt với một tình huống trong đó tôi muốn trả về một giá trị cố định khi tham số đầu vào có một giá trị cụ thể. Vì vậy, nếu chúng ta nói "my_param" bằng "cái gì đó" thì tôi muốn trả về "my_cool_mock"

Điều này dường như có sẵn trên mockito cho python

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

Tôi đã tìm kiếm làm thế nào để đạt được điều tương tự với Mock mà không thành công?

Có ý kiến ​​gì không?


2
Có thể câu trả lời này sẽ giúp - stackoverflow.com/a/7665754/234606
naiquevin

@naiquevin Điều này hoàn toàn giải quyết vấn đề bạn đời, cảm ơn!
Juan Antonio Gomez Moriano

Tôi không biết bạn có thể sử dụng Mocktio với Python, +1 cho điều đó!
Ben

Nếu dự án của bạn sử dụng pytest, cho mục đích như vậy bạn có thể muốn tận dụng monkeypatch. Monkeypatch dành cho "thay thế chức năng này để thử nghiệm", trong khi Mock là những gì bạn sử dụng khi bạn cũng muốn kiểm tra mock_callshoặc đưa ra các xác nhận về những gì nó được gọi và v.v. Có một nơi dành cho cả hai và tôi thường sử dụng cả hai vào những thời điểm khác nhau trong một tệp kiểm tra nhất định.
driftcatcher

Câu trả lời:


187

Nếu side_effectlà một hàm thì bất cứ điều gì hàm đó trả về là những gì gọi đến trả về giả. Các side_effecthàm được gọi với các đối số tương tự như mô hình. Điều này cho phép bạn thay đổi giá trị trả về của cuộc gọi một cách linh hoạt, dựa trên đầu vào:

>>> 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
Chỉ để làm cho câu trả lời dễ dàng hơn, bạn có thể đổi tên hàm side_effect thành cái gì khác không? (tôi biết, tôi biết, nó khá đơn giản, nhưng cải thiện khả năng đọc thực tế là tên hàm và tên param khác nhau :)
Juan Antonio Gomez Moriano

6
@JuanAntonioGomezMoriano Tôi có thể, nhưng trong trường hợp này tôi chỉ trực tiếp trích dẫn tài liệu, vì vậy tôi hơi không thích chỉnh sửa báo giá nếu nó không bị hỏng cụ thể.
Amber

và chỉ để trở thành nhà mô phạm tất cả những năm sau đó, nhưng side effectlà thuật ngữ chính xác: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh

7
@ Tôi không phàn nàn về tên của CallableMixin.side_effect, nhưng hàm riêng biệt được xác định trong ví dụ có cùng tên.
OrangeDog

48

Như đã chỉ ra tại đối tượng Python Mock với phương thức được gọi nhiều lần

Một giải pháp là viết side_effect của riêng tôi

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

Đó là mẹo


2
Điều này làm cho tôi rõ ràng hơn câu trả lời đã chọn, vì vậy cảm ơn bạn đã trả lời câu hỏi của riêng bạn :)
Luca Bezerra

15

Tác dụng phụ có một chức năng (cũng có thể là chức năng lambda ), vì vậy đối với các trường hợp đơn giản, bạn có thể sử dụng:

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

4

Tôi đã kết thúc ở đây để tìm kiếm " làm thế nào để giả lập một hàm dựa trên các đối số đầu vào " và cuối cùng tôi đã giải quyết điều này bằng cách tạo ra một hàm phụ trợ đơn giản:

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

Hiện nay:

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'

Hy vọng điều này sẽ giúp được ai đó!


2

Bạn cũng có thể sử dụng partialtừ functoolsnếu bạn muốn sử dụng một hàm mang theo các thông số nhưng các chức năng bạn đang chế giễu không. Ví dụ như thế này:

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))

Điều này sẽ trả về một cuộc gọi có thể không chấp nhận các tham số (như timezone.now ()) của Django, nhưng hàm mock_year của tôi thì có.


Cảm ơn giải pháp thanh lịch này. Tôi muốn thêm rằng nếu hàm ban đầu của bạn có các tham số bổ sung, chúng cần được gọi với các từ khóa trong mã sản xuất của bạn hoặc cách tiếp cận này sẽ không hoạt động. Bạn nhận được lỗi : got multiple values for argument.
Erik Kalkoken

1

Chỉ để cho thấy một cách làm khác:

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

Bạn cũng có thể sử dụng @mock.patch.object:

Giả sử một mô-đun my_module.pysử dụng pandasđể đọc từ cơ sở dữ liệu và chúng tôi muốn kiểm tra mô-đun này bằng pd.read_sql_tablephương pháp mô phỏng (lấy table_namelàm đối số).

Những gì bạn có thể làm là tạo (trong thử nghiệm của bạn) một db_mockphương thức trả về các đối tượng khác nhau tùy thuộc vào đối số được cung cấp:

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

Trong chức năng kiểm tra của bạn, sau đó bạn làm:

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

Nếu bạn "muốn trả về một giá trị cố định khi tham số đầu vào có một giá trị cụ thể" , có thể bạn thậm chí không cần giả và có thể sử dụng dictcùng với getphương thức của nó :

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

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

Điều này hoạt động tốt khi đầu ra giả của bạn là một ánh xạ đầu vào. Khi đó là chức năng của đầu vào, tôi khuyên bạn nên sử dụng side_effecttheo câu trả lời của Amber .

Bạn cũng có thể sử dụng kết hợp cả hai nếu bạn muốn duy trì Mockkhả năng của mình ( assert_called_once, call_countv.v.):

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

Điều này rất thông minh.
emyller

-6

Tôi biết một câu hỏi khá cũ, có thể giúp cải thiện bằng cách sử dụng lamthon 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.