วิธีการเยาะเย้ยทรัพย์สินแบบอ่านอย่างเดียวด้วยการเยาะเย้ย?


92

คุณล้อเลียนทรัพย์สินแบบอ่านอย่างเดียวด้วยการเยาะเย้ยได้อย่างไร?

ฉันเหนื่อย:

setattr(obj.__class__, 'property_to_be_mocked', mock.Mock())

แต่ปัญหาคือมันใช้กับทุกอินสแตนซ์ของชั้นเรียน ... ซึ่งทำให้การทดสอบของฉันแตก

คุณมีความคิดอื่น ๆ อีกไหม? ฉันไม่ต้องการเยาะเย้ยวัตถุเต็มเฉพาะคุณสมบัติเฉพาะนี้

คำตอบ:


167

ฉันคิดว่าวิธีที่ดีกว่าคือการเยาะเย้ยทรัพย์สินPropertyMockแทนที่จะล้อเลียน__get__วิธีการโดยตรง

มีระบุไว้ในเอกสารให้ค้นหาunittest.mock.PropertyMock: การเยาะเย้ยที่ตั้งใจจะใช้เป็นคุณสมบัติหรือตัวบ่งชี้อื่น ๆ ในคลาส PropertyMockให้__get__และ__set__วิธีการเพื่อให้คุณสามารถระบุค่าส่งคืนเมื่อมีการดึงข้อมูล

นี่คือวิธี:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

def test(unittest.TestCase):
    with mock.patch('MyClass.last_transaction', new_callable=PropertyMock) as mock_last_transaction:
        mock_last_transaction.return_value = Transaction()
        myclass = MyClass()
        print myclass.last_transaction
        mock_last_transaction.assert_called_once_with()

@propertyผมต้องเยาะเย้ยวิธีการเรียนการตกแต่งเป็น คำตอบนี้ใช้ได้ผลสำหรับฉันเมื่อคำตอบอื่น ๆ (และคำตอบอื่น ๆ สำหรับคำถามอื่น ๆ อีกมากมาย) ไม่ได้
AlanSE

3
นี่คือวิธีที่ควรทำ ฉันหวังว่าจะมีวิธีย้ายคำตอบที่ "ยอมรับ"
vitiral

4
ฉันพบว่าการรวมค่าส่งคืนในการเรียกตัวจัดการบริบทให้สะอาดขึ้นเล็กน้อย: `` with mock.patch ('MyClass.last_transaction', new_callable = PropertyMock, return_value = Transaction ()): ... ``
wodow

อันที่จริงฉันเพิ่งย้ายคำตอบที่ยอมรับไปยังคำตอบนี้
charlax

1
การใช้ mock.patch.object ก็ดีเช่นกันเนื่องจากคุณไม่จำเป็นต้องเขียนชื่อคลาสเป็นสตริง (ไม่ใช่ปัญหาในตัวอย่าง) และการตรวจจับ / แก้ไขจะง่ายกว่าหากคุณตัดสินใจเปลี่ยนชื่อแพ็กเกจและไม่ได้ อัปเดตการทดสอบ
Kevin

41

อันที่จริงคำตอบคือ (ตามปกติ) ในเอกสารมันเป็นเพียงการที่ฉันใช้แพทช์กับอินสแตนซ์แทนที่จะเป็นคลาสเมื่อฉันทำตามตัวอย่างของพวกเขา

นี่คือวิธีการ:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

ในชุดทดสอบ:

def test():
    # Make sure you patch on MyClass, not on a MyClass instance, otherwise
    # you'll get an AttributeError, because mock is using settattr and
    # last_transaction is a readonly property so there's no setter.
    with mock.patch(MyClass, 'last_transaction') as mock_last_transaction:
        mock_last_transaction.__get__ = mock.Mock(return_value=Transaction())
        myclass = MyClass()
        print myclass.last_transaction

14
คนควรใช้ตัวอย่างอื่น mock.PropertyMockเป็นวิธีที่ต้องทำ!
vitiral

4
ถูกต้องในขณะที่เขียนPropertyMockไม่มีอยู่
charlax

6

patchถ้าวัตถุที่มีคุณสมบัติที่คุณต้องการที่จะแทนที่เป็นวัตถุจำลองคุณไม่จำเป็นต้องใช้

แต่สามารถสร้างPropertyMockและแทนที่คุณสมบัติในประเภทของการจำลองแทนได้ ตัวอย่างเช่นในการแทนที่mock_rows.pagesคุณสมบัติที่จะส่งคืน(mock_page, mock_page,):

mock_page = mock.create_autospec(reader.ReadRowsPage)
# TODO: set up mock_page.
mock_pages = mock.PropertyMock(return_value=(mock_page, mock_page,))
type(mock_rows).pages = mock_pages

1
แบมแค่สิ่งที่ฉันต้องการ (autospec'd object with a property) และจากเพื่อนร่วมงานไม่น้อย 🙋‍♂️
Mark McDonald

6

อาจเป็นเรื่องของสไตล์ แต่ในกรณีที่คุณชอบมัณฑนากรในการทดสอบคำตอบของ @jamescastlefield อาจเปลี่ยนเป็นดังนี้:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

class Test(unittest.TestCase):
    @mock.patch('MyClass.last_transaction', new_callable=PropertyMock)
    def test(self, mock_last_transaction):
        mock_last_transaction.return_value = Transaction()
        myclass = MyClass()
        print myclass.last_transaction
        mock_last_transaction.assert_called_once_with()

6

ในกรณีที่คุณใช้pytestควบคู่ไปด้วยpytest-mockคุณสามารถทำให้โค้ดของคุณง่ายขึ้นและหลีกเลี่ยงการใช้ตัวจัดการบริบทเช่นwithคำสั่งดังต่อไปนี้:

def test_name(mocker): # mocker is a fixture included in pytest-mock
    mocked_property = mocker.patch(
        'MyClass.property_to_be_mocked',
        new_callable=mocker.PropertyMock,
        return_value='any desired value'
    )
    o = MyClass()

    print(o.property_to_be_mocked) # this will print: any desired value

    mocked_property.assert_called_once_with()

0

หากคุณไม่ต้องการทดสอบว่ามีการเข้าถึงคุณสมบัติจำลองหรือไม่คุณสามารถแก้ไขได้ตามที่คาดreturn_valueไว้

with mock.patch(MyClass, 'last_transaction', Transaction()):
    ...

0

หากคุณต้องการให้คนที่ล้อเลียน@propertyต้องพึ่งพาต้นฉบับ__get__คุณสามารถสร้างแบบกำหนดเองได้MockProperty

class PropertyMock(mock.Mock):

    def __get__(self, obj, obj_type=None):
        return self(obj, obj_type)

การใช้งาน:

class A:

  @property
  def f(self):
    return 123


original_get = A.f.__get__

def new_get(self, obj_type=None):
  return f'mocked result: {original_get(self, obj_type)}'


with mock.patch('__main__.A.f', new_callable=PropertyMock) as mock_foo:
  mock_foo.side_effect = new_get
  print(A().f)  # mocked result: 123
  print(mock_foo.call_count)  # 1
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.