Как мне исправить объект так, чтобы были издевательскими все методы, кроме одного?

У меня есть функция точки входа, вызывающая ее main на объекте, который я хотел бы оставить незащищенным, поскольку она вызывает несколько других методов объекта:

class Thing(object):

    def main(self):
        self.alpha()
        self.bravo()

    def alpha(self):
        self.charlie()

    def bravo(self):
        raise TypeError("Requires Internet connection!")

    def charlie(self):
        raise Exception("Bad stuff happens here!")

Это довольно просто, чтобы смоделировать вручную:

thing = Thing() 
thing.alpha = MagicMock()
thing.bravo = MagicMock()

И я могу протестировать, чтобы убедиться, что оба вызова alpha и bravo вызываются один раз, я могу установить побочные эффекты в alpha и bravo, чтобы убедиться, что они обрабатываются, и т. Д. И т. Д.

Меня беспокоит то, что определение кода изменится и кто-то добавит charlie вызов к main. Его не высмеивают, поэтому теперь будут ощущаться побочные эффекты (и это такие вещи, как запись в файл, подключение к базе данных, получение данных из Интернета, так что это простое исключение не будет предупреждать меня о том, что тесты сейчас плохой).

Мой план состоял в том, чтобы убедиться, что мой фиктивный объект не вызывает никаких других методов, кроме тех, которые я сказал, что он должен (или вызвать тестовое исключение). Однако если я сделаю что-то вроде этого:

MockThing = create_autospec(Thing)
thing = Thing()
thing.main() 

print thing.method_calls
# [calls.main()] 

Затем main также издевается, поэтому он не вызывает никаких других методов. Как я могу издеваться над всеми методами, кроме основного? (Я бы хотел, чтобы method_calls был [calls.alpha(), calls.bravo()]).

Изменить: для взлома ответов

Что ж, у меня есть действительно хакерское решение, но я надеюсь, что есть лучший ответ, чем этот. Обычно я повторно привязываю метод из исходного класса (Python связывает несвязанный метод)

MockThing = create_autospec(Thing)
thing = MockThing()
thing.main = Thing.ingest.__get__(thing, Thing)
thing.main()

print thing.method_calls
# [calls.alpha(), calls.bravo()]

Но должно быть более простое решение, чем использование дескрипторов функций!


person bbengfort    schedule 03.03.2016    source источник
comment
Я получаю сообщение об ошибке, говоря, что объект не имеет атрибута 'ingest'. Есть идеи, что это вызвало? Я использую Python 2.7   -  person Nam G VU    schedule 23.03.2017


Ответы (4)


Когда я делал такие странные вещи, как вызов реального метода класса, над которым я бы издевался, я использовал для вызова статической ссылки на метод:

mt = Mock(Thing)
Thing.main(mt)
print(mt.mock_calls)

[call.alpha(), call.bravo()]

В любом случае, после написания вашего теста лучше разделить его с помощью некоторых соавторов, чтобы отделить то, что вы должны имитировать, от того, что вы хотите протестировать: используйте эти тесты, чтобы провести рефакторинг производственного кода и, наконец, рефакторинг вашего теста, чтобы удалить такие грязные тесты.

person Michele d'Amico    schedule 03.03.2016
comment
Здесь мне помог вызов статической версии метода. Большое спасибо! - person Nam G VU; 23.03.2017

У меня была такая же проблема, но я нашел способ, которым я доволен. В приведенном ниже примере используется ваш класс Thing, как указано выше:

import mock

mock_thing = mock.create_autospec(Thing)
mock_thing.main = lambda x: Thing.main(mock_thing, x)

Это приведет к тому, что mock_thing вызовет фактическую «главную» функцию, принадлежащую mock_thing, но mock_thing.alpha () и mock_thing.beta () будут вызываться как mock! (замените x любыми параметрами, которые вы передаете функции).

Надеюсь, это сработает для вас!

person Ezra    schedule 15.09.2016

Допустим, у вас есть класс Foo с методом экземпляра frobnicate.

Вы можете использовать functools.partial для создания частичной версии функции Foo.frobnicate и привязать ваш фиктивный объект к его первому параметру. Этот способ позволяет вам игнорировать количество аргументов и поддерживать ключевые аргументы (тогда как lambda потребует от вас знания и явной передачи аргументов и не будет поддерживать kwargs).

При таком подходе хорошо определить make_mock_foo функцию, которая создает макет, чтобы привязки через functools.partial хранились в одном месте.

import functools
from unittest import mock


class Foo:
    def frobnicate(self) -> None:
        pass


def make_mock_foo() -> Foo:
    mock_foo = mock.MagicMock(spec=Foo)
    mock_foo.frobnicate = functools.partial(Foo.frobnicate, mock_foo)
    return mock_foo
person Right leg    schedule 03.06.2021

Похоже, вы либо включаете модульное тестирование в свои интеграционные / функциональные тесты, либо беспокоитесь о тестировании других вещей, помимо определенного модуля.

Если вы проводите модульное тестирование Thing, вам следует издеваться над частями, которые вы не тестируете. Но если вы тестируете, как Thing интегрируется с другими вещами (например, с БД), тогда вам следует тестировать свой реальный Thing, а не макет.

Кроме того, именно здесь внедрение зависимостей имеет большой смысл, потому что вы бы сделали что-то вроде этого:

class Thing:
    def __init__(self, db, dangerous_thing):
        self.db = db
        self.dangerous_thing = dangerous_thing

    #....

    def charlie(self):
        foxtrot = self.dangerous_thing.do_it()

Теперь вы можете передать имитацию для dangerous_thing во время тестирования Thing, поэтому вам не нужно беспокоиться о том, действительно выполняет dangerous_thing.

person Wayne Werner    schedule 03.03.2016
comment
Спасибо за совет, Уэйн, но это не ответ на мой вопрос. - person bbengfort; 03.03.2016