Измерить методы на любом экземпляре класса python
Я хочу издеваться над методами в любом экземпляре какого-либо класса в производственном коде, чтобы облегчить тестирование. Есть ли в Python библиотека, которая могла бы облегчить это?
В принципе, я хочу сделать следующее, но в Python (следующий код Ruby, используя библиотеку Mocha):
def test_stubbing_an_instance_method_on_all_instances_of_a_class
Product.any_instance.stubs(:name).returns('stubbed_name')
assert_equal 'stubbed_name', SomeClassThatUsesProduct.get_new_product_name
end
Важно отметить, что мне нужно высмеять это на уровне класса, так как мне действительно нужно издеваться над методами в экземпляре, созданном тем, что я тестирую.
Случай использования:
У меня есть класс QueryMaker
, который вызывает метод в экземпляре RemoteAPI
. Я хочу высмеять метод RemoteAPI.get_data_from_remote_server
, чтобы вернуть некоторую константу. Как это сделать в тесте без необходимости помещать специальный код в код RemoteAPI
, чтобы проверить, в какой среде он работает.
Пример того, что я хотел в действии:
# a.py
class A(object):
def foo(self):
return "A foo"
# b.py
from a import A
class B(object):
def bar(self):
x = A()
return x.foo()
# test.py
from a import A
from b import B
def new_foo(self):
return "New foo"
A.foo = new_foo
y = B()
if y.bar() == "New foo":
print "Success!"
Ответы
Ответ 1
Самый простой способ, вероятно, использовать метод класса. Вы действительно должны использовать метод экземпляра, но это больно для их создания, тогда как есть встроенная функция, которая создает метод класса. С помощью метода класса ваш заглушка получит ссылку на класс (а не на экземпляр) в качестве первого аргумента, но поскольку это заглушка, это, вероятно, не имеет значения. Итак:
Product.name = classmethod(lambda cls: "stubbed_name")
Обратите внимание, что подпись лямбда должна соответствовать сигнатуре метода, который вы заменяете. Кроме того, конечно, поскольку Python (например, Ruby) является динамическим языком, нет никакой гарантии, что кто-то не выключит ваш прошитый метод для чего-то еще, прежде чем вы получите доступ к экземпляру, хотя я ожидаю, что вы будете знать довольно быстро если это произойдет.
Изменить: при дальнейшем исследовании вы можете оставить classmethod()
:
Product.name = lambda self: "stubbed_name"
Я старался как можно ближе сохранить исходное поведение метода, но похоже, что он на самом деле не нужен (и не сохраняет поведение, как я надеялся, так или иначе).
Ответ 2
Нужно обманывать методы, когда тестирование очень распространено, и есть много инструментов, которые помогут вам с ним в Python. Опасность для классов "патчей обезьян", как это, заключается в том, что, если вы не отмените ее после этого, класс был изменен для всех других применений во время ваших тестов.
Моя библиотека mock, которая является одной из самых популярных смехотворных библиотек Python, включает помощника под названием "patch", который помогает вам безопасно исправлять методы или атрибуты объектов и классов во время тестов.
Модуль макета доступен из:
http://pypi.python.org/pypi/mock
Декодер патчей может использоваться как менеджер контекста или как декоратор теста. Вы можете использовать его для исправления с помощью функций самостоятельно или использовать его для автоматического исправления с помощью Mock-объектов, которые очень настраиваются.
from a import A
from b import B
from mock import patch
def new_foo(self):
return "New foo"
with patch.object(A, 'foo', new_foo):
y = B()
if y.bar() == "New foo":
print "Success!"
Это автоматически обрабатывает рассылку. Вы можете уйти, не определяя макетную функцию самостоятельно:
from mock import patch
with patch.object(A, 'foo') as mock_foo:
mock_foo.return_value = "New Foo"
y = B()
if y.bar() == "New foo":
print "Success!"
Ответ 3
Я не знаю Ruby достаточно хорошо, чтобы точно сказать, что вы пытаетесь сделать, но посмотрите __getattr__
метод, Если вы определите его в своем классе, Python вызовет его, когда код попытается получить доступ к любому атрибуту вашего класса, который иначе не определен. Поскольку вы хотите, чтобы это был метод, ему нужно будет создать метод "на лету", который он вернет.
>>> class Product:
... def __init__(self, number):
... self.number = number
... def get_number(self):
... print "My number is %d" % self.number
... def __getattr__(self, attr_name):
... return lambda:"stubbed_"+attr_name
...
>>> p = Product(172)
>>> p.number
172
>>> p.name()
'stubbed_name'
>>> p.get_number()
My number is 172
>>> p.other_method()
'stubbed_other_method'
Также обратите внимание, что __getattr__
не должно использоваться никаких других атрибутов undefined вашего класса, иначе оно будет бесконечно рекурсивным, вызывая __getattr__
для атрибута, который не существует.
... def __getattr__(self, attr_name):
... return self.x
>>> p.y
Traceback (most recent call last):
#clipped
RuntimeError: maximum recursion depth exceeded while calling a Python object
Если это то, что вы хотите сделать только из своего тестового кода, а не производственного кода, затем поместите свое нормальное определение класса в файл производственного кода, затем в тестовом коде определите метод __getattr__
(unbound) и затем привяжите его к классу, который вы хотите:
#production code
>>> class Product:
... def __init__(self, number):
... self.number = number
... def get_number(self):
... print "My number is %d" % self.number
...
#test code
>>> def __getattr__(self, attr):
... return lambda:"stubbed_"+attr_name
...
>>> p = Product(172)
>>> p.number
172
>>> p.name()
Traceback (most recent call last):
File "<interactive input>", line 1, in <module>
AttributeError: Product instance has no attribute 'name'
>>> Product.__getattr__ = __getattr__
>>> p.name()
'stubbed_name'
Я не уверен, как это будет реагировать на класс, который уже использовал __getattribute__
(в отличие от __getattr__
, __getattribute__
вызывается для всех атрибутов независимо от того, существуют они или нет).
Если вы хотите сделать это только для определенных методов, которые уже существуют, вы можете сделать что-то вроде:
#production code
>>> class Product:
... def __init__(self, number):
... self.number = number
... def get_number(self):
... return self.number
...
>>> p = Product(172)
>>> p.get_number()
172
#test code
>>> def get_number(self):
... return "stub_get_number"
...
>>> Product.get_number = get_number
>>> p.get_number()
'stub_get_number'
Или, если вы действительно хотели быть элегантным, вы могли бы создать функцию обертки, чтобы упростить выполнение нескольких методов:
#test code
>>> import functools
>>> def stubber(fn):
... return functools.wraps(fn)(lambda self:"stub_"+fn.__name__)
...
>>> Product.get_number = stubber(Product.get_number)
>>> p.get_number()
'stub_get_number'
Ответ 4
Мак - это способ сделать это, хорошо.
Это может быть немного сложно сделать, чтобы вы исправляли метод экземпляра для всех экземпляров, созданных в классе.
# a.py
class A(object):
def foo(self):
return "A foo"
# b.py
from a import A
class B(object):
def bar(self):
x = A()
return x.foo()
# test.py
from a import A
from b import B
import mock
mocked_a_class = mock.Mock()
mocked_a_instance = mocked_a_class.return_value
mocked_a_instance.foo.return_value = 'New foo'
with mock.patch('b.A', mocked_a_class): # Note b.A not a.A
y = B()
if y.bar() == "New foo":
print "Success!"
Ссылка в docs, при запуске пара. "Чтобы настроить возвращаемые значения для методов экземпляров на исправленном классе..."