Макетирование методов в любом экземпляре класса Python - PullRequest
23 голосов
/ 18 февраля 2011

Я хочу смоделировать методы для любого экземпляра некоторого класса в производственном коде, чтобы облегчить тестирование.Есть ли какая-либо библиотека в 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

Важно отметить, что мне нужно смоделировать это на уровне класса, поскольку мне действительно нужно смоделировать методы в экземпляре, созданном объектом, который я тестирую.

ИспользованиеCase:

У меня есть класс QueryMaker, который вызывает метод для экземпляра RemoteAPI.Я хочу смоделировать метод RemoteAPI.get_data_from_remote_server, чтобы вернуть некоторую константу.Как мне сделать это внутри теста без необходимости помещать специальный код в код RemoteAPI, чтобы проверить, в какой среде он работает.

Пример того, что я хотел в действии:

# a.py
class A(object):
    def foo(self):
        return "A's 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!"

Ответы [ 4 ]

48 голосов
/ 18 февраля 2011

Необходимость макетировать методы, когда тестирование очень распространено, и есть много инструментов, которые помогут вам в Python. Опасность использования таких классов «исправления обезьян» заключается в том, что если вы не отмените впоследствии, тогда класс будет изменен для всех других применений в ваших тестах.

Моя библиотека mock, которая является одной из самых популярных библиотек Python mocking, включает в себя помощник под названием «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!"
2 голосов
/ 10 мая 2017

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

2 голосов
/ 18 февраля 2011

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

Product.name = classmethod(lambda cls: "stubbed_name")

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

Редактировать: При дальнейшем расследовании вы можете пропустить classmethod():

Product.name = lambda self: "stubbed_name"

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

1 голос
/ 18 февраля 2011

Я не достаточно хорошо знаю 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__ нужно , а не использовать любые другиенеопределенные атрибуты вашего класса, иначе он будет бесконечно рекурсивным, вызывая __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'

Или, если вы действительно хотите быть элегантным, вы можете создать функцию-обертку, чтобы сделать несколько методов easy:

#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'
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...