Макет подмножества методов и свойств класса Python

Я использую модуль mock Python для выполнения своих тестов.

Бывают случаи, когда я издеваюсь над классом, однако я просто хочу издеваться над некоторыми его методами и свойствами, а не над всеми.

Предположим следующий сценарий:

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

В приведенном выше коде, как только я исправлю класс SomeClass, вызовы методов, значения return_value которых я явно не установил, вернут MagicMock объектов.

Мой вопрос: как я могу издеваться только над некоторыми методами класса, но при этом сохранять другие?

Есть два способа, которые я могу придумать, но ни один из них не является действительно хорошим.

  1. Один из способов - установить метод макета в исходный метод класса, например:

    some_class_instance.another_method = SomeClass.another_method
    

    На самом деле это нежелательно, потому что у класса может быть много методов и свойств, которые нужно разблокировать.

  2. Другой способ - явно исправить каждый метод, который я хочу, например:

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

    Но на самом деле это не работает, если я хочу издеваться над самим классом, например, для издевательства над вызовами инициализатора. Приведенный ниже код в любом случае переопределит все методы SomeClass.

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

Вот более конкретный пример:

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

В этом случае у меня есть общий метод process_event, который вызывает конкретное событие обработки в зависимости от предоставленного события.

Я хотел бы протестировать только метод process_event. Я просто хочу знать, вызывается ли правильное конкретное событие в зависимости от события, которое я предоставляю.

Итак, в моем тестовом примере я хочу смоделировать только process_event_a и process_event_b, вызвать исходный process_event с определенными параметрами, а затем подтвердить, что process_event_a или process_event_b были вызваны с соответствующими параметрами.


person Felipe Ferri    schedule 31.10.2020    source источник
comment
Вы не должны издеваться над частями того, что должны тестировать. Если вы обнаружите, что хотите частично издеваться над чем-то, это совет, который вам нужно переосмыслить.   -  person jonrsharpe    schedule 31.10.2020
comment
Вы должны использовать только макеты в интерфейсе. Если то, что вы тестируете, нуждается в объекте для игры, и вы хотите, чтобы некоторые из этих объектов были реальными, а другие — ложными, ваш интерфейс, вероятно, слишком велик. Упростите поверхность атаки, чтобы было ясно, где ваши моки должны быть реализованы. Если это неясно, подумайте еще немного о своем дизайне.   -  person Paul Becotte    schedule 31.10.2020
comment
Привет, Джон и Пол, спасибо за ваши ответы. Хорошо, я понимаю, вы думаете, что я не должен этого делать... Значит ли это, что нет лучшего способа сделать это, чем то, что я разместил в вопросе?   -  person Felipe Ferri    schedule 31.10.2020
comment
Ваш пример слишком абстрактен, чтобы говорить об этом. Ваш тест вызывает только макет, поэтому неясно, что вы на самом деле пытаетесь проверить. Используйте тестовые двойники для тестирования их соавторы, в этом случае вам следует тестировать все, что использует SomeClass.   -  person jonrsharpe    schedule 01.11.2020
comment
@jonrsharpe, еще раз спасибо. Я добавил более конкретный пример по вопросу, который, как мне кажется, иллюстрирует случай, когда частичное издевательство над классом имело бы смысл.   -  person Felipe Ferri    schedule 05.11.2020
comment
Этот пример демонстрирует, почему это не работает. Вызывающий process_event просто хочет, чтобы его событие было обработано; тот факт, что вы реализовали это как вызов различных методов, является деталями реализации. Проверив, что вы соединили свои тесты с текущей реализацией.   -  person jonrsharpe    schedule 05.11.2020
comment
Но разве модульные тесты не должны быть связаны с текущей реализацией? Разве модульные тесты не должны быть белым ящиком, где я использую знания о реализации для достижения полного покрытия кода?   -  person Felipe Ferri    schedule 06.11.2020


Ответы (1)


Вместо того, чтобы исправлять весь класс, вы должны исправлять объект. А именно, создайте экземпляр своего класса, а затем исправьте методы этого экземпляра.

Обратите внимание, что вы также можете использовать декоратор @patch.object вместо моего подхода.

class SomeClass:
    def some_method(self):
        return 100

    def another_method(self):
        return 500

В вашем 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()
            )
person Amir Afianian    schedule 31.10.2020
comment
Здравствуйте, Амир! Спасибо за указание на использование patch.object. Пришлось сделать небольшую поправку, потому что при использовании в реализации метода его приходится использовать как контекстный менеджер. - person Felipe Ferri; 05.11.2020