Mock subset dari metode dan properti kelas Python

Oct 31 2020

Saya menggunakan mockmodul Python untuk melakukan pengujian saya.

Ada kalanya saya mengejek kelas, namun saya hanya ingin mengejek beberapa metode dan propertinya, dan tidak semuanya.

Misalkan skenario berikut ini:

# module.py
class SomeClass:
    def some_method(self):
        return 100

    def another_method(self):
        return 500

# test.py
class Tests(unittest.TestCase):
    @patch('module.SomeClass')
    def test_some_operation(self, some_class_mock):
        some_class_instance = some_class_mock.return_value

        # I'm mocking only the some_method method.
        some_class_instance.some_method.return_value = 25

        # This is ok, the specific method I mocked returns the value I wished.
        self.assertEquals(
            25,
            SomeClass().some_method()
        )

        # However, another_method, which I didn't mock, returns a MagicMock instance
        # instead of the original value 500
        self.assertEquals(
            500,
            SomeClass().another_method()
        )

Pada kode di atas, setelah saya menambal SomeClasskelas, panggilan ke metode yang return_values-nya tidak saya setel secara eksplisit akan mengembalikan MagicMockobjek.

Pertanyaan saya adalah: Bagaimana saya bisa mengejek hanya beberapa metode kelas tetapi menjaga yang lain tetap utuh?

Ada dua cara yang bisa saya pikirkan, tetapi tidak ada yang benar-benar baik.

  1. Salah satu caranya adalah dengan menyetel metode tiruan ke metode kelas asli, seperti ini:

    some_class_instance.another_method = SomeClass.another_method
    

    Ini tidak benar-benar diinginkan karena kelas mungkin memiliki banyak metode dan properti untuk "melepaskan".

  2. Cara lain adalah dengan menambal setiap metode yang saya inginkan secara eksplisit, seperti:

     @patch('module.SomeClass.some_method')
     def test_some_operation(self, some_method_mock):
    

    Tetapi ini tidak benar-benar berfungsi jika saya ingin mengejek kelas itu sendiri, untuk mengejek panggilan ke penginisialisasi misalnya. Kode di bawah ini akan menimpa semua SomeClassmetode.

     @patch('module.SomeClass.some_method')
     @patch('module.SomeClass')
     def test_some_operation(self, some_class_mock, some_method_mock):
    

Berikut adalah contoh yang lebih spesifik:

class Order:
    def process_event(self, event, data):
        if event == 'event_a':
            return self.process_event_a(data)

        elif event == 'event_b':
            return self.process_event_b(data)

        else:
            return None

    def process_event_a(self, data):
        # do something with data

    def process_event_b(self, data):
        # do something different with data

Dalam hal ini, saya memiliki metode umum process_eventyang memanggil acara pemrosesan tertentu tergantung pada acara yang disediakan.

Saya hanya ingin menguji metodenya process_event. Saya hanya ingin tahu apakah acara khusus yang tepat dipanggil tergantung pada acara yang saya sediakan.

Jadi, dalam kasus pengujian saya, yang ingin saya lakukan adalah mengejek adil process_event_adan process_event_b, memanggil yang asli process_eventdengan parameter tertentu, lalu menegaskan salah satu process_event_aatau process_event_bdipanggil dengan parameter yang tepat.

Jawaban

1 AmirAfianian Oct 31 2020 at 21:14

Alih-alih menambal seluruh kelas, Anda harus menambal objek. Yaitu, buat instance kelas Anda, lalu tambal metode dari instance itu.

Perhatikan bahwa Anda juga dapat menggunakan dekorator @patch.objectdaripada pendekatan saya.

class SomeClass:
    def some_method(self):
        return 100

    def another_method(self):
        return 500

Di Anda test.py

from unittest import mock

class Tests(unittest.TestCase):
    

    def test_some_operation(self):
        some_class_instance = SomeClass()

        # I'm mocking only the some_method method.
        with mock.patch.object(some_class_instance, 'some_method', return_value=25) as cm:

            # This is gonna be ok
            self.assertEquals(
                25,
                SomeClass().some_method()
            )

            # The other methods work as they were supposed to.
            self.assertEquals(
                500,
                SomeClass().another_method()
            )