Mengejek fungsi python berdasarkan argumen input

150

Kami telah menggunakan Mock untuk python untuk sementara waktu.

Sekarang, kita memiliki situasi di mana kita ingin mengejek suatu fungsi

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

Biasanya, cara untuk mengejek ini adalah (dengan asumsi foo menjadi bagian dari objek)

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

Bahkan, jika saya memanggil foo () beberapa kali saya dapat menggunakan

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

Sekarang, saya menghadapi situasi di mana saya ingin mengembalikan nilai tetap ketika parameter input memiliki nilai tertentu. Jadi jika katakan "my_param" sama dengan "sesuatu" maka saya ingin mengembalikan "my_cool_mock"

Ini sepertinya tersedia di mockito untuk python

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

Saya telah mencari cara untuk mencapai hal yang sama dengan Mock tanpa keberhasilan?

Ada ide?

Juan Antonio Gomez Moriano
sumber
2
Mungkin jawaban ini akan membantu - stackoverflow.com/a/7665754/234606
naiquevin
@ naquevin Ini dengan sempurna menyelesaikan masalah, terima kasih!
Juan Antonio Gomez Moriano
Saya tidak tahu Anda bisa menggunakan Mocktio dengan Python, +1 untuk itu!
Ben
Jika proyek Anda menggunakan pytest, untuk tujuan seperti itu Anda mungkin ingin memanfaatkan monkeypatch. Monkeypatch lebih untuk "mengganti fungsi ini demi pengujian," sedangkan Mock adalah apa yang Anda gunakan ketika Anda juga ingin memeriksa mock_callsatau membuat pernyataan tentang apa yang dipanggil dengan dan sebagainya. Ada tempat untuk keduanya, dan saya sering menggunakan keduanya pada waktu yang berbeda dalam file tes yang diberikan.
driftcatcher
Kemungkinan duplikat objek Python Mock dengan metode yang disebut beberapa kali
Melebius

Jawaban:

187

Jika side_effectada fungsi maka apa pun fungsi itu kembali adalah apa yang panggilan ke tiruan kembali. The side_effectfungsi disebut dengan argumen yang sama seperti pura-pura. Ini memungkinkan Anda memvariasikan nilai balik panggilan secara dinamis, berdasarkan input:

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

Amber
sumber
25
Hanya untuk membuat jawabannya lebih mudah, bisakah Anda mengubah nama fungsi side_effect menjadi sesuatu yang lain? (saya tahu, saya tahu, itu cukup sederhana, tetapi meningkatkan keterbacaan fakta bahwa nama fungsi dan nama param berbeda :)
Juan Antonio Gomez Moriano
6
@JuanAntonioGomezMoriano saya bisa, tetapi dalam hal ini saya hanya langsung mengutip dokumentasi, jadi saya agak enggan untuk mengedit kutipan jika tidak secara khusus rusak.
Amber
dan hanya untuk menjadi bertele-tele bertahun-tahun kemudian, tetapi side effectadalah istilah yang akurat: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh
7
@Ish mereka tidak mengeluh tentang nama CallableMixin.side_effect, tetapi bahwa fungsi terpisah yang didefinisikan dalam contoh memiliki nama yang sama.
OrangeDog
48

Seperti yang ditunjukkan pada objek Python Mock dengan metode yang disebut beberapa kali

Solusinya adalah menulis sendiri side_effect saya

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

Itu yang berhasil

Juan Antonio Gomez Moriano
sumber
2
Ini membuatnya lebih jelas bagi saya daripada jawaban yang dipilih, jadi terima kasih telah menjawab pertanyaan Anda sendiri :)
Luca Bezerra
15

Efek samping mengambil fungsi (yang juga bisa menjadi fungsi lambda ), jadi untuk kasus sederhana Anda dapat menggunakan:

m = MagicMock(side_effect=(lambda x: x+1))
Shubham Chaudhary
sumber
4

Saya telah berakhir di sini mencari " cara mengejek fungsi berdasarkan argumen masukan " dan saya akhirnya menyelesaikan ini dengan membuat fungsi aux sederhana:

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

Sekarang:

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'

Semoga ini bisa membantu seseorang!

Manu
sumber
2

Anda juga dapat menggunakan partialdari functoolsjika Anda ingin menggunakan fungsi yang mengambil parameter tetapi fungsi yang Anda cemooh tidak. Misal seperti ini:

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

Ini akan mengembalikan callable yang tidak menerima parameter (seperti timezone.now Django), tetapi fungsi mock_year saya tidak.

Hernan
sumber
Terima kasih atas solusi elegan ini. Saya ingin menambahkan bahwa jika fungsi asli Anda memiliki parameter tambahan, mereka perlu dipanggil dengan kata kunci dalam kode produksi Anda atau pendekatan ini tidak akan berfungsi. Anda mendapatkan error: got multiple values for argument.
Erik Kalkoken
1

Hanya untuk menunjukkan cara lain untuk melakukannya:

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
caleb
sumber
1

Anda juga dapat menggunakan @mock.patch.object:

Katakanlah modul my_module.pymenggunakan pandasuntuk membaca dari database dan kami ingin menguji modul ini dengan pd.read_sql_tablemetode mengejek (yang table_namedianggap sebagai argumen).

Apa yang dapat Anda lakukan adalah untuk membuat (dalam uji Anda) sebuah db_mockmetode yang kembali objek yang berbeda tergantung pada argumen yang disediakan:

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

Dalam fungsi tes Anda, Anda kemudian lakukan:

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`
Tomasz Bartkowiak
sumber
1

Jika Anda "ingin mengembalikan nilai tetap ketika parameter input memiliki nilai tertentu" , mungkin Anda bahkan tidak memerlukan tiruan dan dapat menggunakan dictbersama dengan getmetodenya:

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

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

Ini bekerja dengan baik ketika output palsu Anda adalah pemetaan input. Ketika itu adalah fungsi input saya sarankan menggunakan side_effectsesuai jawaban Amber .

Anda juga dapat menggunakan kombinasi keduanya jika Anda ingin mempertahankan Mockkemampuan ( assert_called_once, call_countdll):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get
Arseniy Panfilov
sumber
Ini sangat pintar.
emyller
-6

Saya tahu ini pertanyaan yang cukup lama, mungkin bisa membantu sebagai perbaikan menggunakan 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
Pemula
sumber