Стверджуйте, що метод був викликаний в модульному тесті Python


91

Припустимо, я маю такий код в модульному тесті Python:

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

Чи є простий спосіб ствердити, що певний метод (у моєму випадку aw.Clear()) був викликаний під час другого рядка тесту? наприклад, чи є щось подібне:

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

Відповіді:


150

Я використовую Mock (який зараз є unittest.mock на py3.3 +) для цього:

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)

Для вашого випадку це може виглядати так:

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 підтримує чимало корисних функцій, включаючи способи виправлення об’єкта або модуля, а також перевірку того, що було викликано потрібну річ тощо тощо.

Будьте готові! (Покупець остерігайтеся!)

Якщо ви неправильно введете assert_called_with(до assert_called_onceабо assert_called_wiht), ваш тест все ще може працювати, оскільки Mock вважатиме, що це знущальна функція, і із задоволенням піде, якщо ви не використовуєте autospec=true. Для отримання додаткової інформації читайте assert_called_once: Загроза або загроза .


5
+1 за дискретне просвітлення мого світу за допомогою чудового модуля Mock.
Рон Коен,

@RonCohen: Так, це досить дивно, і постійно покращується. :)
Макке

1
Хоча використання макету - це безсумнівно шлях, я б радив не використовувати assert_called_once, просто не існує :)
FelixCQ

його було видалено в пізніших версіях. Мої тести все ще використовують його. :)
Macke

1
Варто повторити, наскільки корисно використовувати autospec = True для будь-якого знущаного об'єкта, оскільки він дійсно може вас вкусити, якщо ви неправильно напишете метод затвердження.
rgilligan

30

Так, якщо ви використовуєте Python 3.3+. Ви можете використовувати вбудований unittest.mockметод затвердження, який називається. Для Python 2.6+ використовуйте рухомий бекпорт Mock, це те саме.

Ось короткий приклад у вашому випадку:

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

14

Я не знаю нічого вбудованого. Це досить просто реалізувати:

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

Для цього потрібно, щоб сам об’єкт не змінював self.b, що майже завжди відповідає дійсності.


Я сказав, що мій Python був іржавим, хоча я протестував своє рішення, щоб переконатися, що воно працює :-) Я узагальнив Python до версії 2.5, насправді я ніколи не використовував 2.5 для будь-якого значущого Python, оскільки нам довелося заморозити на 2.3 для сумісності lib. Переглядаючи ваше рішення, я знайшов effbot.org/zone/python-with-statement.htm як гарний чіткий опис. Я б покірно припустив, що мій підхід виглядає меншим і може бути простішим у застосуванні, якщо ви хочете більше, ніж одну точку реєстрації, а не вкладені "з". Я б дуже хотів, щоб ви пояснили, чи є якісь ваші переваги.
Andy Dent,

@Andy: Ваша відповідь менша, оскільки вона часткова: вона насправді не тестує результати, не відновлює початкову функцію після тесту, щоб ви могли продовжувати використовувати об'єкт, і вам доведеться кілька разів писати код, щоб зробити все що знову кожного разу, коли ви пишете тест. Кількість рядків коду підтримки не важлива; цей клас використовується у власному модулі тестування, а не в рядку документації - для цього потрібно один-два рядки коду у фактичному тесті.
Гленн Мейнард,

6

Так, я можу дати вам схему, але мій Python трохи іржавий, і я занадто зайнятий, щоб детально пояснювати.

В основному, вам потрібно вставити проксі в метод, який буде викликати оригінал, наприклад:

 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)

Ця відповідь StackOverflow про виклик може допомогти вам зрозуміти вищезазначене.

Більш докладно:

Хоча відповідь була прийнята, завдяки цікавій дискусії з Гленом і маючи кілька хвилин вільного часу, я хотів розширити свою відповідь:

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

приємно. Я додав кількість викликів до methCallLogger, щоб я міг заявити про це.
Марк Хіт,

Це над ретельним, самостійним рішенням, яке я запропонував? Серйозно?
Гленн Мейнард,

@Glenn Я дуже новачок у Python - можливо, твій кращий - я просто ще не все це розумію. Пізніше я витрачу трохи часу, щоб спробувати.
Mark Heath

Це, безумовно, найпростіша і зрозуміла відповідь. Дійсно приємна робота!
Метт Мессерсміт,

4

Ви можете висміяти aw.Clearабо вручну, або використовуючи тестовий фреймворк, наприклад pymox . Вручну ви зробите це, використовуючи щось подібне:

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

Використовуючи pymox, ви зробите це так:

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

Мені теж подобається такий підхід, хоча я все ще хочу, щоб old_clear викликався. Це дає зрозуміти, що відбувається.
Mark Heath
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.