Khẳng định rằng một phương thức đã được gọi trong một bài kiểm tra đơn vị Python


91

Giả sử tôi có mã sau trong bài kiểm tra đơn vị Python:

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

Có cách nào dễ dàng để khẳng định rằng một phương thức cụ thể (trong trường hợp của tôi aw.Clear()) đã được gọi trong dòng thứ hai của bài kiểm tra không? ví dụ: có một cái gì đó như thế này:

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

Câu trả lời:


150

Tôi sử dụng Mock (hiện là unittest.mock trên py3.3 +) cho việc này:

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)

Đối với trường hợp của bạn, nó có thể trông như thế này:

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 hỗ trợ khá nhiều tính năng hữu ích, bao gồm các cách vá một đối tượng hoặc mô-đun, cũng như kiểm tra xem đúng thứ đã được gọi hay chưa, v.v.

Emptor caveat! (Người mua hãy cẩn thận!)

Nếu bạn nhập sai assert_called_with(thành assert_called_oncehoặc assert_called_wiht), thử nghiệm của bạn có thể vẫn chạy, vì Mock sẽ nghĩ đây là một chức năng bị chế nhạo và vui vẻ thực hiện, trừ khi bạn sử dụng autospec=true. Để biết thêm thông tin, hãy đọc khẳng định_called_once: Đe doạ hoặc đe doạ .


5
+1 để tận mắt khám phá thế giới của tôi với mô-đun Mock tuyệt vời.
Ron Cohen

@RonCohen: Vâng, nó khá tuyệt vời và luôn tốt hơn. :)
Macke

1
Trong khi sử dụng mô hình chắc chắn là con đường để đi, tôi muốn khuyên bạn không dùng assert_called_once, với chỉ đơn giản là không tồn tại :)
FelixCQ

nó đã bị loại bỏ trong các phiên bản sau. Các bài kiểm tra của tôi vẫn đang sử dụng nó. :)
Macke

1
Cần nhắc lại mức độ hữu ích của việc sử dụng autospec = True cho bất kỳ đối tượng bị chế nhạo nào vì nó thực sự có thể khiến bạn khó chịu nếu bạn viết sai chính tả phương thức khẳng định.
rgilligan

30

Có nếu bạn đang sử dụng Python 3.3+. Bạn có thể sử dụng unittest.mockphương thức tích hợp để xác nhận được gọi. Đối với Python 2.6+, hãy sử dụng backport cuộn Mock, điều này cũng tương tự.

Đây là một ví dụ nhanh trong trường hợp của bạn:

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

14

Tôi không biết bất kỳ thứ gì được tích hợp sẵn. Nó khá đơn giản để thực hiện:

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

Điều này đòi hỏi bản thân đối tượng sẽ không sửa đổi self.b, điều này hầu như luôn đúng.


Tôi đã nói rằng Python của tôi đã bị gỉ, mặc dù tôi đã thử nghiệm giải pháp của mình để đảm bảo nó hoạt động :-) Tôi đã nội bộ hóa Python trước phiên bản 2.5, thực tế là tôi chưa bao giờ sử dụng 2.5 cho bất kỳ Python đáng kể nào vì chúng tôi phải đóng băng ở 2.3 để tương thích với lib. Khi xem xét giải pháp của bạn, tôi thấy effbot.org/zone/python-with-statement.htm là một mô tả rõ ràng hay. Tôi khiêm tốn đề xuất cách tiếp cận của tôi trông nhỏ hơn và có thể dễ áp ​​dụng hơn nếu bạn muốn nhiều hơn một điểm ghi nhật ký, thay vì "với" được lồng vào nhau. Tôi thực sự muốn bạn giải thích nếu có bất kỳ lợi ích cụ thể nào của bạn.
Andy Dent

@Andy: Câu trả lời của bạn nhỏ hơn vì nó là một phần: nó không thực sự kiểm tra kết quả, nó không khôi phục chức năng ban đầu sau khi kiểm tra để bạn có thể tiếp tục sử dụng đối tượng và bạn phải viết mã nhiều lần để thực hiện tất cả một lần nữa mỗi khi bạn viết một bài kiểm tra. Số lượng dòng mã hỗ trợ không quan trọng; lớp này đi trong mô-đun thử nghiệm của riêng nó, không phải nội tuyến trong chuỗi doc - điều này cần một hoặc hai dòng mã trong thử nghiệm thực tế.
Glenn Maynard

6

Có, tôi có thể cung cấp cho bạn bản phác thảo nhưng Python của tôi hơi thiếu và tôi quá bận để giải thích chi tiết.

Về cơ bản, bạn cần đặt một proxy trong phương thức sẽ gọi bản gốc, ví dụ:

 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)

Câu trả lời StackOverflow này về callable có thể giúp bạn hiểu những điều trên.

Chi tiết hơn:

Mặc dù câu trả lời đã được chấp nhận, do cuộc thảo luận thú vị với Glenn và có một vài phút rảnh rỗi, tôi muốn phóng to câu trả lời của mình:

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

đẹp. Tôi đã thêm số lượng cuộc gọi vào methCallLogger để tôi có thể khẳng định về nó.
Mark Heath

Điều này so với giải pháp toàn diện, khép kín mà tôi đã cung cấp? Nghiêm túc?
Glenn Maynard

@Glenn Tôi rất mới với Python - có thể cái của bạn tốt hơn - tôi chỉ chưa hiểu hết về nó. Tôi sẽ dành một chút thời gian sau để thử nó.
Mark Heath

Đây là câu trả lời đơn giản và dễ hiểu nhất. Công việc thực sự tốt đẹp!
Matt Messersmith

4

Bạn có thể mô phỏng aw.Clearthủ công hoặc sử dụng khung thử nghiệm như pymox . Theo cách thủ công, bạn sẽ làm điều đó bằng cách sử dụng một cái gì đó như sau:

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

Sử dụng pymox, bạn sẽ làm như thế này:

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

Tôi cũng thích cách tiếp cận này, mặc dù tôi vẫn muốn old_clear được gọi. Điều này làm cho nó rõ ràng những gì đang xảy ra.
Mark Heath
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.