ยืนยันว่ามีการเรียกเมธอดในการทดสอบหน่วย Python


94

สมมติว่าฉันมีรหัสต่อไปนี้ในการทดสอบหน่วย Python:

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

มีวิธีง่ายๆในการยืนยันว่ามีการเรียกวิธีการเฉพาะ (ในกรณีของฉันaw.Clear()) ในระหว่างบรรทัดที่สองของการทดสอบหรือไม่? เช่นมีบางอย่างเช่นนี้:

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

คำตอบ:


153

ฉันใช้Mock (ซึ่งตอนนี้เป็นunittest.mockบน py3.3 +) สำหรับสิ่งนี้:

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)

สำหรับกรณีของคุณอาจมีลักษณะดังนี้:

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 รองรับคุณสมบัติที่มีประโยชน์ไม่กี่อย่างรวมถึงวิธีการแก้ไขวัตถุหรือโมดูลตลอดจนการตรวจสอบว่ามีการเรียกสิ่งที่ถูกต้อง ฯลฯ เป็นต้น

ถ้ำมอง! (ผู้ซื้อระวัง!)

หากคุณพิมพ์ผิดassert_called_with(เพื่อassert_called_onceหรือassert_called_wiht) autospec=trueการทดสอบของคุณอาจยังคงทำงานเป็นจำลองจะคิดว่านี่เป็นฟังก์ชั่นล้อเลียนและมีความสุขไปพร้อมเว้นแต่คุณจะใช้ สำหรับข้อมูลเพิ่มเติมอ่านassert_called_once: ภัยคุกคามหรืออันตราย


5
+1 สำหรับการทำให้โลกของฉันกระจ่างใสอย่างไม่น่าเชื่อด้วยโมดูลจำลองที่ยอดเยี่ยม
Ron Cohen

@RonCohen: ใช่มันน่าทึ่งมากและก็ดีขึ้นตลอดเวลาด้วย :)
Macke

1
ในขณะที่การใช้การเยาะเย้ยเป็นวิธีที่จะไปได้ฉันไม่แนะนำให้ใช้ assert_called_once แต่ก็ไม่มีอยู่จริง :)
FelixCQ

มันถูกลบออกในรุ่นที่ใหม่กว่า การทดสอบของฉันยังคงใช้อยู่ :)
Macke

1
ควรทำซ้ำว่าการใช้ autospec = True มีประโยชน์อย่างไรสำหรับวัตถุที่ถูกเยาะเย้ยเพราะมันสามารถกัดคุณได้หากคุณสะกดผิดวิธีการยืนยัน
rgilligan

31

ใช่ถ้าคุณใช้ Python 3.3+ คุณสามารถใช้unittest.mockเมธอดในตัวเพื่อยืนยันที่เรียกว่า สำหรับ Python 2.6+ ให้ใช้ Rolling backport Mockซึ่งก็เหมือนกัน

นี่คือตัวอย่างสั้น ๆ ในกรณีของคุณ:

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

14

ฉันไม่รู้ว่ามีอะไรอยู่ในตัว มันค่อนข้างง่ายที่จะใช้:

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

สิ่งนี้ต้องการให้อ็อบเจ็กต์เองไม่แก้ไข self.b ซึ่งเกือบจะเป็นจริงเสมอ


ฉันบอกว่า Python ของฉันเป็นสนิมแม้ว่าฉันจะทดสอบวิธีแก้ปัญหาเพื่อให้แน่ใจว่ามันใช้งานได้ :-) ฉันทำให้ Python ภายในเวอร์ชัน 2.5 ในความเป็นจริงฉันไม่เคยใช้ 2.5 สำหรับ Python ที่มีนัยสำคัญใด ๆ เพราะเราต้องหยุดที่ 2.3 เพื่อความเข้ากันได้ของ lib ในการตรวจสอบโซลูชันของคุณฉันพบว่าeffbot.org/zone/python-with-statement.htmเป็นคำอธิบายที่ชัดเจน ฉันขอแนะนำอย่างถ่อมตนว่าแนวทางของฉันดูเล็กลงและอาจนำไปใช้ได้ง่ายกว่าหากคุณต้องการบันทึกมากกว่าหนึ่งจุดแทนที่จะเป็น "ด้วย" ที่ซ้อนกัน ฉันอยากให้คุณอธิบายว่าประโยชน์ของคุณมีอะไรบ้าง
Andy Dent

@ แอนดี้: คำตอบของคุณมีขนาดเล็กลงเนื่องจากเป็นเพียงบางส่วน: ไม่ได้ทดสอบผลลัพธ์จริง แต่จะไม่คืนค่าฟังก์ชันดั้งเดิมหลังการทดสอบเพื่อให้คุณสามารถใช้วัตถุต่อไปได้และคุณต้องเขียนโค้ดซ้ำ ๆ เพื่อทำทั้งหมด อีกครั้งทุกครั้งที่คุณเขียนแบบทดสอบ จำนวนบรรทัดของรหัสสนับสนุนไม่สำคัญ คลาสนี้อยู่ในโมดูลการทดสอบของตัวเองไม่ใช่แบบอินไลน์ใน docstring ซึ่งใช้โค้ดหนึ่งหรือสองบรรทัดในการทดสอบจริง
Glenn Maynard

6

ใช่ฉันสามารถให้โครงร่างได้ แต่ Python ของฉันค่อนข้างเป็นสนิมและฉันยุ่งเกินไปที่จะอธิบายรายละเอียด

โดยทั่วไปคุณต้องใส่พร็อกซีในวิธีการที่จะเรียกใช้ต้นฉบับเช่น:

 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)

นี้คำตอบ StackOverflowเกี่ยวกับ callable อาจช่วยให้คุณเข้าใจดังกล่าวข้างต้น

รายละเอียดเพิ่มเติม:

แม้ว่าคำตอบจะได้รับการยอมรับเนื่องจากการสนทนาที่น่าสนใจกับ Glenn และมีเวลาว่างไม่กี่นาทีฉันต้องการขยายคำตอบของฉัน:

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

ดี. ฉันได้เพิ่มจำนวนการโทรลงใน methCallLogger เพื่อให้ฉันสามารถยืนยันได้
Mark Heath

นี่เป็นวิธีแก้ปัญหาที่มีอยู่ในตัวเองอย่างละเอียด? อย่างจริงจัง?
Glenn Maynard

@Glenn ฉันยังใหม่กับ Python มาก - บางทีอันที่คุณดีกว่า - ฉันยังไม่เข้าใจทั้งหมด ฉันจะใช้เวลาสักครู่ในการลองใช้
Mark Heath

นี่เป็นคำตอบที่ง่ายและเข้าใจง่ายที่สุด ทำได้ดีจริงๆ!
Matt Messersmith

4

คุณสามารถเยาะเย้ยออกaw.Clearด้วยตนเองหรือโดยใช้กรอบการทดสอบเช่นpymox ด้วยตนเองคุณจะทำโดยใช้สิ่งนี้:

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

ใช้ pymox คุณจะทำดังนี้:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)

ฉันชอบวิธีนี้เช่นกันแม้ว่าฉันยังต้องการให้ old_clear เรียก สิ่งนี้ทำให้เห็นได้ชัดว่าเกิดอะไรขึ้น
Mark Heath
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.