Membuat panggilan secara berurutan ke metode tiruan

175

Mock memiliki metode yang bermanfaatassert_called_with() . Namun, sejauh yang saya mengerti ini hanya memeriksa panggilan terakhir ke suatu metode.
Jika saya memiliki kode yang memanggil metode mocked 3 kali berturut-turut, setiap kali dengan parameter yang berbeda, bagaimana saya bisa menegaskan 3 panggilan ini dengan parameter spesifik mereka?

Jonathan
sumber

Jawaban:

179

assert_has_calls adalah pendekatan lain untuk masalah ini.

Dari dokumen:

assert_has_calls (panggilan, any_order = Salah)

menegaskan mock telah dipanggil dengan panggilan yang ditentukan. Daftar mock_calls diperiksa untuk panggilan.

Jika any_order salah (default) maka panggilan harus berurutan. Mungkin ada panggilan tambahan sebelum atau setelah panggilan yang ditentukan.

Jika any_order Benar maka panggilan bisa dalam urutan apa pun, tetapi semuanya harus muncul di mock_calls.

Contoh:

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

Sumber: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls

Pigueiras
sumber
9
Agak aneh mereka memilih untuk menambahkan tipe "panggilan" baru yang mereka juga bisa saja menggunakan daftar atau tuple ...
jaapz
@jaapz Ini subkelas tuple: isinstance(mock.call(1), tuple)memberi True. Mereka juga menambahkan beberapa metode dan atribut.
jpmc26
13
Versi awal Mock menggunakan tuple biasa, tetapi ternyata canggung untuk digunakan. Setiap panggilan fungsi menerima tupel (args, kwargs), jadi untuk memeriksa bahwa "foo (123)" dipanggil dengan benar, Anda perlu "menegaskan mock.call_args == ((123,), {})", yang merupakan seteguk dibandingkan dengan "panggilan (123)"
Jonathan Hartley
Apa yang Anda lakukan ketika pada setiap instance panggilan Anda mengharapkan nilai pengembalian yang berbeda?
CodeWithPride
2
@CodeWithPride sepertinya lebih banyak pekerjaan untukside_effect
Pigueiras
108

Biasanya, saya tidak peduli dengan urutan panggilan, hanya saja itu terjadi. Dalam hal ini, saya menggabungkan assert_any_calldengan pernyataan tentang call_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

Saya merasa melakukannya dengan cara ini agar lebih mudah dibaca dan dipahami daripada sejumlah besar panggilan masuk ke satu metode.

Jika Anda benar-benar peduli dengan pesanan atau mengharapkan beberapa panggilan identik, assert_has_callsmungkin lebih tepat.

Edit

Sejak saya memposting jawaban ini, saya telah memikirkan kembali pendekatan saya pada pengujian secara umum. Saya pikir itu layak disebutkan bahwa jika pengujian Anda mendapatkan ini rumit, Anda mungkin menguji secara tidak tepat atau memiliki masalah desain. Mock dirancang untuk menguji komunikasi antar-objek dalam desain berorientasi objek. Jika desain Anda tidak berorientasi pada objek (seperti dalam lebih prosedural atau fungsional), tiruan itu mungkin sama sekali tidak pantas. Anda juga mungkin memiliki terlalu banyak hal yang terjadi di dalam metode ini, atau Anda mungkin menguji rincian internal yang sebaiknya tidak diolok-olok. Saya mengembangkan strategi yang disebutkan dalam metode ini ketika kode saya tidak terlalu berorientasi objek, dan saya percaya saya juga menguji detail internal yang sebaiknya dibiarkan tidak terbongkar.

jpmc26
sumber
@ jpmc26 dapatkah Anda menjelaskan lebih lanjut tentang hasil edit Anda? Apa yang Anda maksud dengan 'dibiarkan paling baik tanpa diejek'? Bagaimana lagi Anda akan menguji jika panggilan telah dilakukan dalam suatu metode
otgw
@emo Sering, lebih baik membiarkan metode yang sebenarnya dipanggil. Jika metode lain rusak, itu mungkin merusak tes, tetapi nilai menghindari yang lebih kecil dari nilai memiliki tes yang lebih sederhana, lebih dapat dipertahankan. Waktu terbaik untuk mengejek adalah ketika panggilan eksternal ke metode lain adalah apa yang ingin Anda uji (Biasanya, ini berarti beberapa jenis hasil diteruskan ke dalamnya dan kode yang diuji tidak mengembalikan hasil.) Atau metode lain memiliki dependensi eksternal (database, situs web) yang ingin Anda hapus. (Secara teknis, kasus terakhir lebih merupakan sebuah rintisan, dan saya akan ragu untuk menegaskannya.)
jpmc26
@ jpmc26 mengejek berguna ketika Anda ingin menghindari injeksi ketergantungan atau metode pemilihan strategi runtime lainnya. seperti yang Anda sebutkan, menguji logika dalam metode, tanpa memanggil layanan eksternal, dan yang lebih penting, tanpa menyadari lingkungan (tidak, tidak, untuk kode yang baik do() if TEST_ENV=='prod' else dont()), dicapai dengan mudah dengan mengejek seperti yang Anda sarankan. efek samping dari hal ini adalah untuk mempertahankan tes per versi (katakanlah perubahan kode antara google search api v1 dan v2, kode Anda akan menguji versi 1 tidak peduli apa)
Daniel Dubovski
@DanielDubovski Sebagian besar pengujian Anda harus berbasis input / output. Itu tidak selalu mungkin, tetapi jika itu tidak mungkin sebagian besar waktu, Anda mungkin memiliki masalah desain. Ketika Anda membutuhkan beberapa nilai yang dikembalikan yang biasanya berasal dari potongan kode lain dan Anda ingin memotong ketergantungan, sebuah rintisan biasanya akan melakukannya. Mengejek hanya diperlukan ketika Anda perlu memverifikasi bahwa beberapa fungsi memodifikasi negara (mungkin tanpa nilai kembali) dipanggil (Perbedaan antara tiruan dan rintisan adalah bahwa Anda tidak menegaskan pada panggilan dengan rintisan.) Menggunakan tiruan di mana rintisan akan melakukan membuat tes Anda kurang dapat dipertahankan.
jpmc26
@ jpmc26 tidak memanggil layanan eksternal semacam output? tentu saja Anda dapat menolak kode yang membangun pesan yang akan dikirim dan mengujinya alih-alih menegaskan params panggilan, tetapi IMHO, itu hampir sama. Bagaimana Anda menyarankan untuk mendesain ulang memanggil API eksternal? Saya setuju bahwa mengejek harus dikurangi seminimal mungkin, semua yang saya katakan adalah Anda tidak bisa menguji data yang Anda kirim ke layanan eksternal untuk memastikan logika berperilaku seperti yang diharapkan.
Daniel Dubovski
47

Anda dapat menggunakan Mock.call_args_listatribut untuk membandingkan parameter dengan panggilan metode sebelumnya. Sehubungan dengan Mock.call_countatribut harus memberi Anda kontrol penuh.

Jonathan
sumber
9
assert_has_calls ()?
bavaza
5
assert_has_callshanya memeriksa apakah panggilan yang diharapkan telah dilakukan, tetapi tidak jika itu adalah satu-satunya.
blueyed
17

Saya selalu harus melihat ini berulang kali, jadi inilah jawaban saya.


Menyatakan beberapa panggilan metode pada objek yang berbeda dari kelas yang sama

Misalkan kita memiliki kelas tugas berat (yang ingin kita tiru):

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

berikut adalah beberapa kode yang menggunakan dua instance HeavyDutykelas:

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


Sekarang, ini adalah test case untuk heavy_workfungsinya:

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

Kami mengejek HeavyDutykelas dengan MockHeavyDuty. Untuk menegaskan panggilan metode yang datang dari setiap HeavyDutycontoh, kita harus merujuk MockHeavyDuty.return_value.assert_has_calls, bukan MockHeavyDuty.assert_has_calls. Selain itu, dalam daftar expected_callskami harus menentukan nama metode yang kami minati untuk menyatakan panggilan. Jadi daftar kami dibuat dari panggilan call.do_work, bukan hanya call.

Melatih uji kasus menunjukkan kepada kita bahwa itu berhasil:

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


Jika kami memodifikasi heavy_workfungsinya, pengujian gagal dan menghasilkan pesan kesalahan yang membantu:

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


Membuat beberapa panggilan ke suatu fungsi

Untuk kontras dengan yang di atas, berikut adalah contoh yang menunjukkan cara mengejek banyak panggilan ke suatu fungsi:

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


Ada dua perbedaan utama. Yang pertama adalah bahwa ketika mengejek suatu fungsi, kami mengatur panggilan yang kami harapkan menggunakan call, bukannya menggunakan call.some_method. Yang kedua adalah bahwa kita sebut assert_has_callspada mock_work_function, bukan pada mock_work_function.return_value.

Pedro M Duarte
sumber