Мне нужно проверить последовательность вызова некоторых методов класса.
У меня была такая потребность, когда я разрабатывал с использованием TDD (разработка через тестирование), поэтому, когда я писал тест для method_1(), я хотел быть уверен, что он вызывает некоторые методы в точном порядке.
Я предполагаю, что моя продукция class A хранится в следующем файле class_a.py:
class A:
__lock = None
def __init__(self, lock):
self.__lock = lock
def method_1(self):
self.__lock.acquire()
self.__atomic_method_1()
self.__lock.release()
def __atomic_method_1(self):
pass
Атрибут __lock является экземпляром класса threading.Lock и используется для достижения потокобезопасного выполнения __atomic_method_1().
Мне нужно написать модульный тест, который проверяет последовательность вызова методов class A при вызове method_1().
Проверка должна подтвердить, что method_1() вызывает:
self.__lock.acquire()self.__atomic_method_1()self.__lock.release()Моя потребность исходит из желания убедиться, что метод __atomic_method_1() работает в многопоточном контексте без прерывания.
Это очень полезная ссылка, но мою проблему она не решает. Решение, предоставленное по ссылке, идеально подходит для проверки порядка вызова последовательности функций, вызываемых другой функцией. Ссылка показывает пример, в котором тестируемая функция и вызываемые функции содержатся в файле с именем module_under_test.
Однако в моем случае мне нужно проверить последовательность вызова методов класса, и это различие не позволяет использовать решение, предоставленное по ссылке.
Однако я попытался разработать модульный тест, ссылаясь на ссылку, и таким образом я подготовил трассировку тестового файла, которую могу показать ниже:
import unittest
from unittest import mock
from class_a import A
import threading
class TestCallOrder(unittest.TestCase):
def test_call_order(self):
# create an instance of the SUT (note the parameter threading.Lock())
sut = A(threading.Lock())
# prepare the MOCK object
source_mock = ...
with patch(...)
# prepare the expected values
expected = [...]
# run the code-under-test (method_1()).
sut.method_1()
# Check the order calling
self.assertEqual(expected, source_mock.mock_calls)
if __name__ == '__main__':
unittest.main()
Но я не могу завершить метод test_call_order().
Спасибо
Что не так в первом ответе по вашей ссылке? Кажется, это именно то, что вам нужно.
@duffymo Я согласен с вами и вообще не делаю подобный тест. В этом случае я хочу быть уверенным, что методы acquire() и release() класса threading.Lock вызываются первыми и последними. Это гарантирует, что инструкции внутри __atomic_method_1() потокобезопасны.
@jprebys Ссылка работает, если функции не являются методами класса. Идея ссылки идеальна, но я не могу использовать ее именно в своей задаче. Если кто-то может адаптировать информацию внутри ссылки к моему классу, я был бы признателен.
Я бы не пошел так далеко. Просто мое мнение.






В комментариях к вопросу они сказали, что это не то, для чего предназначены модульные тесты. Да, это делает тесты хрупкими. Но они служат реальному использованию: правильно ли я реализую блокировку. Возможно, вы захотите реорганизовать свой класс, чтобы его было легче тестировать (и это был бы еще один интересный вопрос).
Но если вы действительно хотите проверить это как есть, у меня есть решение для вас.
Что нам нужно, так это шпионить за тремя методами self.__lock.acquire, self.__lock.release и self.__atomic_method_1. Один из способов сделать это — обернуть вокруг них Mock и записать поведение. Но просто знать, что их призвали, недостаточно, вам нужен порядок между ними. Поэтому вам нужно несколько шпионов, которые коллективно регистрируют происходящие действия.
import unittest
from unittest import mock
from class_a import A
import threading
class TestCallOrder(unittest.TestCase):
def test_call_order(self):
sut = A(threading.Lock())
# bypass the "name mangling" due to leading "__"
sut_lock = sut._A__lock
sut_method = sut._A__atomic_method_1
# store what we observed during the test
observed = []
# prepare the side effects : they are simply observing the call, then forwarding it to the actual function
def lock_acquire_side_effect():
observed.append("lock acquired")
sut_lock.acquire()
def lock_release_side_effect():
observed.append("lock released")
sut_lock.release()
def method_side_effect(*args, **kwargs):
observed.append("method called")
sut_method(*args, **kwargs)
# prepare the spies, one on the lock, one on the method
# (we could also mock the two Lock methods separately)
lock_spy = mock.Mock(wraps=sut_lock, **{"acquire.side_effect": lock_acquire_side_effect,
"release.side_effect": lock_release_side_effect})
method_spy = mock.Mock(wraps=sut_method, **{"side_effect": method_side_effect})
# they are wrapping the actual object, so method calls and attribute access are forwarded to it
# but we also configure them, for certain calls, to use a special side effect (our spy functions)
with mock.patch.object(sut, "_A__lock", lock_spy):
with mock.patch.object(sut, "_A__atomic_method_1", method_spy):
# we apply both spies (with Python 3.10 you can do it with only one `with`)
sut.method_1()
self.assertEqual(["lock acquired", "method called", "lock released"], observed)
# and we check against a very nice ordered log of observations
if __name__ == '__main__':
unittest.main()
Чтобы лучше объяснить, что я сделал, вот схема того, как вещи связаны без моков:
Ваш SUT имеет две ссылки:
__locked, который указывает на его экземпляр Lock, который сам имеет (на наш взгляд) 2 ссылки: acquire и release__atomic_method_1, который указывает на его A.__atomic_method_1 методМы хотим наблюдать за вызовами __lock.acquire, __lock.release, __atomic_method_1 и их относительным порядком.
Более простой способ сделать то, что я мог придумать, - заменить каждую из этих трех «шпионскими функциями», которые записывают, как они вызывались (просто добавляя в список), а затем перенаправлять вызов фактической функции.
Но тогда нам нужно, чтобы эти функции вызывались, поэтому нам придется издеваться над вещами. Поскольку они не являются «импортируемыми», мы не можем их mock.patch . Но у нас есть фактический объект, над которым мы хотим издеваться, и это именно то, для чего предназначен mock.patch.object! Находясь в with, sut.something будет заменен макетом. Итак, нам нужно два макета, один для __atomic_method_1, другой для __lock.
Насколько я могу судить, мы не будем использовать __atomic_method_1 каким-либо другим способом, кроме его вызова. Поэтому мы просто хотим, чтобы вместо этого наш макет вызывал наш шпионский метод. Для этого мы можем настроить его для вызова функции при ее вызове, обозначенной "side_effect".
Но есть много других способов, которыми мы можем использовать наш __lock помимо acquire-ing и release-ing его. Мы не знаем, что __aotmic_method_1 сделает с этим. Поэтому, чтобы быть уверенным, мы установим mock для пересылки всего фактическому объекту, что означает, что это wraps оно.
Что дает нам это:
Звонки на __lock.acquire и __lock.release как бы перенаправляются (благодаря насмешкам) через нашего шпиона, в то время как все остальные по-прежнему проходят нормально.
(We could have done without creating a Mock for __aotmic_method_1, and mock.patch.object with the spy function)
Он отлично работает, но я должен изучить его, прежде чем принять ответ. Но для меня очень полезно. Пока я могу только проголосовать за него. Извините, ваши знания намного выше моих.
@frankfalse Я добавил объяснение с красочными схемами :) Надеюсь, так станет понятнее. Но на самом деле насмешки могут быть трудными, когда мы хотим делать такие тонкие вещи. Раньше мне было трудно, нужно время, чтобы освоить моки.
Большое спасибо. Ваше объяснение настолько ясно, что «я вынужден» принять ваш ответ. Мне нужно изучить инструкцию mock.Mock(wraps=sut_method, **{"side_effect": method_side_effect}), потому что я ее не понимаю. Если вы хотите добавить краткий пример об этом в ответ, я был бы признателен. Я нашел другое решение своей проблемы, и после того, как я принял ваш ответ, я добавляю свое плохое решение. Если хотите, пожалуйста, добавьте к нему комментарии.
Для этой конкретной инструкции я рекомендую вам прочитать документ mock.Mock , в нем объясняется, что делает wraps, и что он принимает «произвольные аргументы ключевого слова» для configure_mock дальше. Синтаксис немного странный для него, я мог бы сделать просто Mock(side_effect=method_side_effect), но это не сработало бы для другого, вы не можете сделать Mock(release.side_effect=lock_release_side_effect). Это яснее?
Ясно. Я должен прочитать документацию. Большое спасибо.
Я изучил инструкцию sut_spy = mock.Mock(wraps=sut, **{...}) и создал несколько полезных тестов по ней. Я прочитал эту документацию, но она для меня не так ясна. Лучше напишите несколько примеров, начиная с кода вашего ответа. Один вопрос: до вашего примера я не знал об использовании нотации **{'key': 'value'} внутри вызова метода, а только в определении метода. Я думаю, что ничего не знаю о нотациях Python. Вы можете помочь мне? Я должен написать новый вопрос?
@frankfalse Функция mock.Mock принимает kwargs, аргументы с произвольными именами, и действует на них. Например, если вы дадите ему foo=4, то он установит возвращенный макет так, чтобы он имел поле foo, значение которого равно 4 (см. configure). Я хочу, чтобы он отвечал на acquire.side_effect, но синтаксис Python не позволяет мне писать acquire.side_effect=lock_acquire_side_effect в вызове функции, потому что acquire не существует. Поэтому я использую ** для прямой передачи словаря, ключами которого будут именованные параметры, см. этот вопрос.
Спасибо: вашего последнего комментария и этого вопроса достаточно, чтобы добавить больше понимания о передаче словаря с оператором ** в функцию и о том, как вы использовали этот оператор в своем тестовом коде.
Решение @Lenormju явно богато концепциями о Mocking и, на мой взгляд, предпочтительнее этого. На самом деле я принял это. Однако я предлагаю другой ответ, который можно использовать для решения некоторых проблем тестирования и моего конкретного тестового примера.
Метод тестирования, который я написал, основан на следующих идеях:
mock.create_autospec():mock_a = mock.create_autospec(A)
method_1() классом A и передать ему экземпляр Mock:A.method_1(mock_a)
Полный тестовый файл выглядит следующим образом:
import unittest
from unittest import mock
from class_a import A
class MyTestCase(unittest.TestCase):
def test_call_order(self):
mock_a = mock.create_autospec(A)
expected = [mock.call._A__lock.acquire(),
mock.call._A__atomic_method_1(),
mock.call._A__lock.release()]
A.method_1(mock_a)
self.assertEqual(expected, mock_a.mock_calls)
if __name__ == '__main__':
unittest.main()
Одна из проблем этого теста заключается в том, что он не создает экземпляр class A и, следовательно, вызывает method_1() по-другому в отношении производственного кода.
С другой стороны, это специальный тест, который я должен использовать для проверки статической структуры кода method_1(), поэтому, на мой взгляд, и только в этом конкретном случае, трюк может быть приемлемым.
Действительно, мое решение намного сложнее вашего. Основное отличие в том, что мой шпионит за звонками, а ваш их полностью издевается. Вы вообще не проверяете, что делает __atomic_method_1, если бы он (случайно) сделал lock.__release, ваш тест не увидел бы его и не прошел. Но если вы просто хотите убедиться, что method_1 делаете эти три ожидаемые вещи в ожидаемом порядке, то ваше решение гораздо больше подходит для проблемы, чем мое, я просто не понимал, что фокус был настолько узким. Рада, что вы нашли хороший способ! :)
@Lenormju Я думаю, что в будущем у меня будут другие подобные проблемы с тестированием, поэтому я надеюсь, что вы окажете мне и всем, кто в этом нуждается, такой уровень поддержки. Огромное спасибо.
Я не думаю, что это хорошее использование модульного тестирования. Модульные тесты методов должны быть делом черного ящика. Тестируйте метод_1, а не его реализацию.