Tegaskan bahwa sebuah metode dipanggil dalam pengujian unit Python

91

Misalkan saya memiliki kode berikut dalam pengujian unit Python:

aw = aps.Request("nv1")
aw2 = aps.Request("nv2", aw)

Adakah cara mudah untuk menyatakan bahwa metode tertentu (dalam kasus saya aw.Clear()) dipanggil selama baris kedua pengujian? misalnya apakah ada yang seperti ini:

#pseudocode:
assertMethodIsCalled(aw.Clear, lambda: aps.Request("nv2", aw))
Mark Heath
sumber

Jawaban:

150

Saya menggunakan Mock (yang sekarang unittest.mock di py3.3 +) untuk ini:

from mock import patch
from PyQt4 import Qt


@patch.object(Qt.QMessageBox, 'aboutQt')
def testShowAboutQt(self, mock):
    self.win.actionAboutQt.trigger()
    self.assertTrue(mock.called)

Untuk kasus Anda, ini akan terlihat seperti ini:

import mock
from mock import patch


def testClearWasCalled(self):
   aw = aps.Request("nv1")
   with patch.object(aw, 'Clear') as mock:
       aw2 = aps.Request("nv2", aw)

   mock.assert_called_with(42) # or mock.assert_called_once_with(42)

Mock mendukung beberapa fitur yang berguna, termasuk cara untuk menambal objek atau modul, serta memeriksa apakah panggilan yang benar, dll.

Caveat emptor! (Pembeli berhati-hatilah!)

Jika Anda salah mengetik assert_called_with( assert_called_onceatau assert_called_wiht) pengujian Anda mungkin masih berjalan, karena Mock akan mengira ini adalah fungsi yang diolok-olok dan dengan senang hati berjalan, kecuali Anda menggunakannya autospec=true. Untuk info lebih lanjut baca assert_called_once: Ancaman atau Ancaman .

Macke
sumber
5
1 untuk secara diam-diam mencerahkan dunia saya dengan modul Mock yang luar biasa.
Ron Cohen
@RonCohen: Ya, itu sangat menakjubkan, dan menjadi lebih baik setiap saat. :)
Macke
1
Sementara menggunakan tiruan jelas merupakan cara yang harus dilakukan, saya menyarankan agar tidak menggunakan assert_called_once, dengan tidak ada :)
FelixCQ
itu telah dihapus di versi yang lebih baru. Tes saya masih menggunakannya. :)
Macke
1
Perlu diulangi betapa bermanfaatnya menggunakan autospec = True untuk objek tiruan apa pun karena itu benar-benar dapat menggigit Anda jika Anda salah mengeja metode assert.
rgilligan
30

Ya jika Anda menggunakan Python 3.3+. Anda dapat menggunakan unittest.mockmetode bawaan untuk menegaskan yang dipanggil. Untuk Python 2.6+ gunakan rolling backport Mock, yang merupakan hal yang sama.

Berikut adalah contoh singkat dalam kasus Anda:

from unittest.mock import MagicMock
aw = aps.Request("nv1")
aw.Clear = MagicMock()
aw2 = aps.Request("nv2", aw)
assert aw.Clear.called
Devy
sumber
14

Saya tidak mengetahui apa pun yang ada di dalamnya. Ini cukup sederhana untuk diterapkan:

class assertMethodIsCalled(object):
    def __init__(self, obj, method):
        self.obj = obj
        self.method = method

    def called(self, *args, **kwargs):
        self.method_called = True
        self.orig_method(*args, **kwargs)

    def __enter__(self):
        self.orig_method = getattr(self.obj, self.method)
        setattr(self.obj, self.method, self.called)
        self.method_called = False

    def __exit__(self, exc_type, exc_value, traceback):
        assert getattr(self.obj, self.method) == self.called,
            "method %s was modified during assertMethodIsCalled" % self.method

        setattr(self.obj, self.method, self.orig_method)

        # If an exception was thrown within the block, we've already failed.
        if traceback is None:
            assert self.method_called,
                "method %s of %s was not called" % (self.method, self.obj)

class test(object):
    def a(self):
        print "test"
    def b(self):
        self.a()

obj = test()
with assertMethodIsCalled(obj, "a"):
    obj.b()

Ini mensyaratkan bahwa objek itu sendiri tidak akan memodifikasi self.b, yang hampir selalu benar.

Glenn Maynard
sumber
Saya mengatakan Python saya berkarat, meskipun saya menguji solusi saya untuk memastikannya berfungsi :-) Saya menginternalisasi Python sebelum versi 2.5, sebenarnya saya tidak pernah menggunakan 2.5 untuk Python yang signifikan karena kami harus membekukan di 2.3 untuk kompatibilitas lib. Dalam meninjau solusi Anda, saya menemukan effbot.org/zone/python-with-statement.htm sebagai deskripsi yang bagus dan jelas. Saya dengan rendah hati menyarankan pendekatan saya terlihat lebih kecil dan mungkin lebih mudah diterapkan jika Anda menginginkan lebih dari satu titik logging, daripada bertingkat "dengan". Saya sangat ingin Anda menjelaskan jika ada manfaat khusus dari Anda.
Andy Dent
@Andy: Jawaban Anda lebih kecil karena parsial: tidak benar-benar menguji hasil, tidak mengembalikan fungsi asli setelah pengujian sehingga Anda dapat terus menggunakan objek, dan Anda harus berulang kali menulis kode untuk melakukan semua itu lagi setiap kali Anda menulis tes. Jumlah baris kode dukungan tidak penting; kelas ini masuk ke modul pengujiannya sendiri, bukan sebaris di docstring - ini membutuhkan satu atau dua baris kode dalam pengujian sebenarnya.
Glenn Maynard
6

Ya, saya bisa memberi Anda garis besarnya tetapi Python saya agak berkarat dan saya terlalu sibuk untuk menjelaskan secara detail.

Pada dasarnya, Anda perlu menempatkan proxy dalam metode yang akan memanggil yang asli, misalnya:

 class fred(object):
   def blog(self):
     print "We Blog"


 class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth

   def __call__(self, code=None):
     self.meth()
     # would also log the fact that it invoked the method

 #example
 f = fred()
 f.blog = methCallLogger(f.blog)

Ini jawaban StackOverflow tentang callable dapat membantu Anda memahami di atas.

Lebih detail:

Meskipun jawabannya diterima, karena diskusi yang menarik dengan Glenn dan memiliki waktu luang beberapa menit, saya ingin memperbesar jawaban saya:

# helper class defined elsewhere
class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth
     self.was_called = False

   def __call__(self, code=None):
     self.meth()
     self.was_called = True

#example
class fred(object):
   def blog(self):
     print "We Blog"

f = fred()
g = fred()
f.blog = methCallLogger(f.blog)
g.blog = methCallLogger(g.blog)
f.blog()
assert(f.blog.was_called)
assert(not g.blog.was_called)
Andy Dent
sumber
bagus. Saya telah menambahkan jumlah panggilan ke methCallLogger sehingga saya dapat menegaskannya.
Mark Heath
Ini atas solusi menyeluruh dan mandiri yang saya berikan? Sungguh?
Glenn Maynard
@ Glenn Saya sangat baru mengenal Python - mungkin yang Anda lebih baik - saya hanya belum mengerti semuanya. Saya akan menghabiskan sedikit waktu nanti untuk mencobanya.
Mark Heath
Sejauh ini, ini adalah jawaban yang paling sederhana dan paling mudah dipahami. Kerja yang sangat bagus!
Matt Messersmith
4

Anda dapat membuat tiruan aw.Clear, baik secara manual atau menggunakan kerangka pengujian seperti pymox . Secara manual, Anda akan melakukannya menggunakan sesuatu seperti ini:

class MyTest(TestCase):
  def testClear():
    old_clear = aw.Clear
    clear_calls = 0
    aw.Clear = lambda: clear_calls += 1
    aps.Request('nv2', aw)
    assert clear_calls == 1
    aw.Clear = old_clear

Menggunakan pymox, Anda akan melakukannya seperti ini:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)
Max Shawabkeh
sumber
Saya juga suka pendekatan ini, meskipun saya masih ingin old_clear dipanggil. Ini memperjelas apa yang sedang terjadi.
Mark Heath