Почему не получается замокать декоратор у функции?
Всем привет я пытаюсь написать юнит тест.
У меня есть декоратор который проверяет является ли аргумент переданный в функцию действительно аргументом ожидаемого типа. @check_types_method
Его использования выглядит вот так:
class Calculator: @check_types_method def call(self, a: MegaNumber, b: int) -> int: return a.value + b |
class Calculator: @check_types_method def call(self, a: MegaNumber, b: int) -> int: return a.value + b
Если переменная a будет не MegaNumber то будет брошено исключение.
Это круто работает но при тестах это создает проблемы.
Например мне надо подменить MegaNumber mock объектом и если я это сделаю то в тесте получаю ошибку.
TypeError: The argument 'a' must be of type <class 'mega_number.MegaNumber'>, received: MagicMock |
TypeError: The argument 'a' must be of type <class 'mega_number.MegaNumber'>, received: MagicMock
Для теста нужно замокать декоратор что бы он не чего не делал. И пропускал Mock объекты. Однако у меня не как не получается сделать это и в чем причина я не пойму.
Моя лучшая попытка выглядит так:
import unittest from unittest.mock import MagicMock, patch from calculator import Calculator @patch('calculator.check_types_method', lambda x: x) class TestCalculator(unittest.TestCase): def test_call(self): mock_mega_number = MagicMock() mock_mega_number.value = 10 calculator = Calculator() result = calculator.call(mock_mega_number, 5) self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's " "value and the integer provided.") |
import unittest from unittest.mock import MagicMock, patch from calculator import Calculator @patch('calculator.check_types_method', lambda x: x) class TestCalculator(unittest.TestCase): def test_call(self): mock_mega_number = MagicMock() mock_mega_number.value = 10 calculator = Calculator() result = calculator.call(mock_mega_number, 5) self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's " "value and the integer provided.")
но декоратор загружается раньше чем применяется patch - и я не как не могу победить это. Подскажите как быть?
код для воспроизведения:
check_types_method.py
from inspect import signature from functools import wraps from typing import get_origin, get_args, Union def check_types_method(func): @wraps(func) def wrapper(*args, **kwargs): sig = signature(func) bound = sig.bind(*args, **kwargs) bound.apply_defaults() for name, value in bound.arguments.items(): expected_type = sig.parameters[name].annotation if expected_type is not sig.empty: if not is_of_generic_type(value, expected_type): raise TypeError( f"The argument '{name}' must be of type {expected_type}, received: {type(value).__name__}") return func(*args, **kwargs) def is_of_generic_type(obj, generic_type): origin_type = get_origin(generic_type) arg_types = get_args(generic_type) if origin_type is Union: return any(is_of_generic_type(obj, arg) for arg in arg_types) if origin_type is None: # Non-generic types return isinstance(obj, generic_type) if not isinstance(obj, origin_type): return False if origin_type in (list, set): element_type = arg_types[0] return all(is_of_generic_type(item, element_type) for item in obj) elif origin_type is dict: key_type, value_type = arg_types return all(is_of_generic_type(k, key_type) and is_of_generic_type(v, value_type) for k, v in obj.items()) elif origin_type is tuple: if len(arg_types) == 2 and arg_types[1] is Ellipsis: return all(isinstance(item, arg_types[0]) for item in obj) else: return len(obj) == len(arg_types) and all( is_of_generic_type(item, t) for item, t in zip(obj, arg_types)) return False return wrapper |
from inspect import signature from functools import wraps from typing import get_origin, get_args, Union def check_types_method(func): @wraps(func) def wrapper(*args, **kwargs): sig = signature(func) bound = sig.bind(*args, **kwargs) bound.apply_defaults() for name, value in bound.arguments.items(): expected_type = sig.parameters[name].annotation if expected_type is not sig.empty: if not is_of_generic_type(value, expected_type): raise TypeError( f"The argument '{name}' must be of type {expected_type}, received: {type(value).__name__}") return func(*args, **kwargs) def is_of_generic_type(obj, generic_type): origin_type = get_origin(generic_type) arg_types = get_args(generic_type) if origin_type is Union: return any(is_of_generic_type(obj, arg) for arg in arg_types) if origin_type is None: # Non-generic types return isinstance(obj, generic_type) if not isinstance(obj, origin_type): return False if origin_type in (list, set): element_type = arg_types[0] return all(is_of_generic_type(item, element_type) for item in obj) elif origin_type is dict: key_type, value_type = arg_types return all(is_of_generic_type(k, key_type) and is_of_generic_type(v, value_type) for k, v in obj.items()) elif origin_type is tuple: if len(arg_types) == 2 and arg_types[1] is Ellipsis: return all(isinstance(item, arg_types[0]) for item in obj) else: return len(obj) == len(arg_types) and all( is_of_generic_type(item, t) for item, t in zip(obj, arg_types)) return False return wrapper
mega_number.py
class MegaNumber: def __init__(self): self.value = 10 |
class MegaNumber: def __init__(self): self.value = 10
calculator.py
from check_types_method import check_types_method from mega_number import MegaNumber class Calculator: @check_types_method def call(self, a: MegaNumber, b: int) -> int: return a.value + b |
from check_types_method import check_types_method from mega_number import MegaNumber class Calculator: @check_types_method def call(self, a: MegaNumber, b: int) -> int: return a.value + b
test_calculator.py
import unittest from unittest.mock import MagicMock, patch from calculator import Calculator @patch('calculator.check_types_method', lambda x: x) class TestCalculator(unittest.TestCase): def test_call(self): mock_mega_number = MagicMock() mock_mega_number.value = 10 calculator = Calculator() result = calculator.call(mock_mega_number, 5) self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's " "value and the integer provided.") |
import unittest from unittest.mock import MagicMock, patch from calculator import Calculator @patch('calculator.check_types_method', lambda x: x) class TestCalculator(unittest.TestCase): def test_call(self): mock_mega_number = MagicMock() mock_mega_number.value = 10 calculator = Calculator() result = calculator.call(mock_mega_number, 5) self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's " "value and the integer provided.")
Дополнительно:
Ответы:
Мне кажется, ты просто используешь неверный инструмент, пытаясь совместить несовместимое.
Вариант А
Ты проверяешь, что у тебя объект нужного типа. Тогда твой декоратор в принципе работает, но вообще-то это задача для статического анализатора кода типа mypy или встроенного в pycharm, а не проверки в рантайме. И тогда нужно забыть про моки.
Вариант Б
Ты проверяешь, что у тебя объект имеет нужные поля и методы (duck typing).
Тогда тебе нужен typing.Protocol в комбинации с typing.runtime_checkable, с помощью которого ты сможешь описать, что должен иметь объект. Затем этот протокол можно будет подсунуть в isinstance() для проверки, и мок, по идее, её пройдёт. Но опять-таки, задача скорее для статического анализатора кода, чем для рантайм-проверки. Если у тебя в принципе неведомо что может быть передано в метод - это простыми тестами не решается.
- Ну если чувак написал динамическую проверку типов - то это не от хорошей жизни верно?
Я просто ловил разные приколы с конвертацией (не явной) типов данных питоне которые не палятся твоими статическими анализаторами. А когда софт считает бабло (тем более твое) как то хочется перестраховаться. - MasterCopipaster, ну так используй готовый инструмент, тот же pydantic, а то что ты сейчас делаешь, больше похоже на переусложнение на ровном месте.
А ларчик то просто открывался
import unittest from unittest.mock import MagicMock, patch from calculator import Calculator from mega_number import MegaNumber class TestCalculator(unittest.TestCase): def test_call(self): mock_mega_number = MagicMock(MegaNumber) mock_mega_number.value = 10 calculator = Calculator() result = calculator.call(mock_mega_number, 5) self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's " "value and the integer provided.") |
import unittest from unittest.mock import MagicMock, patch from calculator import Calculator from mega_number import MegaNumber class TestCalculator(unittest.TestCase): def test_call(self): mock_mega_number = MagicMock(MegaNumber) mock_mega_number.value = 10 calculator = Calculator() result = calculator.call(mock_mega_number, 5) self.assertEqual(result, 15, "The call method should return the sum of MegaNumber's " "value and the integer provided.")
Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.
Пока нет других ответов. Будьте первым, кто поможет автору.
Ответить на вопрос
Для начала давайте разберемся, что такое декораторы в программировании. Декоратор - это паттерн проектирования, который позволяет добавлять новое поведение или функциональность существующему объекту, не изменяя его структуру. Это достигается путем обертывания объекта в другой объект, который добавляет нужное поведение.
Теперь вернемся к вашей проблеме с замоканием декоратора у функции. Возможно, у вас есть функция, которую вы хотите декорировать, чтобы добавить ей новое поведение. Давайте посмотрим на пример на языке PHP:
function myFunction() { echo "Original function"; } function myDecorator($func) { return function() use ($func) { echo "Before function call"; $func(); echo "After function call"; }; } $decoratedFunction = myDecorator('myFunction'); $decoratedFunction();
В данном примере у нас есть функция `myFunction`, которую мы хотим декорировать. Мы создаем функцию `myDecorator`, которая принимает функцию в качестве аргумента и возвращает новую функцию, которая добавляет нужное нам поведение.
Однако, возможно у вас возникает проблема с передачей аргументов в декоратор. Если вам нужно передать аргументы в декоратор, то вы можете использовать замыкание (closure) для этого. Например:
function myFunction($name) { echo "Hello, $name"; } function myDecorator($func) { return function($name) use ($func) { echo "Before function call"; $func($name); echo "After function call"; }; } $decoratedFunction = myDecorator('myFunction'); $decoratedFunction('Alice');
Таким образом, вы можете использовать замыкания для передачи аргументов в декораторы функций. Надеюсь, это поможет вам решить проблему с замоканием декоратора у функции.