2011-02-18 22 views
15

Quiero simular métodos en cualquier instancia de alguna clase en el código de producción para facilitar las pruebas. ¿Hay alguna biblioteca en Python que pueda facilitar esto?Métodos de burla en cualquier instancia de una clase python

Básicamente, quiero hacer lo siguiente, pero en Python (el siguiente código es Ruby, utilizando la biblioteca 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 

Lo importante observar desde arriba es que necesito para burlarse de él en el nivel de clase, ya que en realidad necesito burlar los métodos en una instancia creada por lo que estoy probando.

Caso de uso:

tengo una clase QueryMaker el que llama a un método en una instancia de RemoteAPI. Quiero simular el método RemoteAPI.get_data_from_remote_server para devolver algo constante. ¿Cómo puedo hacer esto dentro de una prueba sin tener que poner un caso especial dentro del código RemoteAPI para comprobar qué entorno se está ejecutando en

Ejemplo de lo que quería en acción:.

# 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!" 

Respuesta

1

forma más fácil es probablemente para usar un método de clase. Deberías usar un método de instancia, pero crearlas te resulta doloroso, mientras que hay una función incorporada que crea un método de clase. Con un método de clase, su stub obtendrá una referencia a la clase (en lugar de la instancia) como primer argumento, pero dado que es un apéndice, probablemente no importe. Por lo tanto:

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

Tenga en cuenta que la firma de la lambda debe coincidir con la firma del método que está reemplazando. También, por supuesto, dado que Python (como Ruby) es un lenguaje dinámico, no hay garantía de que alguien no cambie su método stubbed por otra cosa antes de tener en sus manos la instancia, aunque espero que lo sepa muy rápidamente. si eso pasa.

Editar: En una investigación adicional, se puede dejar de lado el classmethod():

Product.name = lambda self: "stubbed_name" 

yo estaba tratando de preservar el comportamiento del método original lo más fielmente posible, pero parece que en realidad no es necesario (y doesn' preservar el comportamiento como esperaba, de todos modos).

+0

No estoy siguiendo exactamente cómo funciona esto. ¿Esto satisface mi caso de uso? –

+1

Debería; su caso de uso es más o menos el mismo que su ejemplo. Básicamente, al cambiar la definición del método en la clase, también está cambiando su definición en todas las instancias. La función 'classmethod' crea un contenedor que básicamente convierte la función en un método, y' lambda' define la función para simplemente devolver una cadena estática. – kindall

+0

... pero vea mi edición. – kindall

1

No conozco a Ruby lo suficiente como para decir exactamente lo que estás tratando de hacer, pero echa un vistazo al método __getattr__. Si lo define en su clase, Python lo llamará cuando el código intente acceder a cualquier atributo de su clase que no esté definido de otra manera. Como desea que sea un método, deberá crear un método sobre la marcha que regrese.

>>> 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' 

También tenga en cuenta que las necesidades de __getattr__ no utiliza ningún otros atributos no definidos de su clase, o de lo contrario será infinitamente recursiva, llamando __getattr__ para el atributo que no existe.

...  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 

Si esto es algo que sólo quiere hacer de su código de prueba, no el código de producción, a continuación, poner su definición de clase normal en el archivo de código de producción, a continuación, en el código de prueba a definir el __getattr__ método (no unida) , y luego se unen a la clase que desea:

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

no estoy seguro de cómo iba a reaccionar con una clase que ya estaba utilizando __getattribute__ (en contraposición a __getattr__, __getattribute__ se llama para todos los atributos o no Ellos existen).

Si sólo quiere hacer esto para los métodos específicos que ya existen, entonces se podría hacer algo como:

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

O si realmente quería ser elegante, se puede crear una función de contenedor para hacer hacer varios métodos fáciles:

#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' 
+0

El objetivo de este ejercicio es evitar modificar cualquiera de los códigos de producción solo por el hecho de poder probarlo. Estoy bastante seguro de que lo que estás sugiriendo no logra eso. –

+0

Si eso es lo que quieres hacer, estoy bastante seguro de que puedes definirlo como una función independiente en tu código de prueba, y luego vincularlo a la clase desde tu código de prueba a través de 'Product .__ getattr__ = fn_name' – user470379

+0

Entiendo qué' __getattr__' se usa para, y realmente no entiendo por qué piensas que sería útil. Necesito reemplazar un método que ya está definido en la clase en la que necesito simular el método. –

35

Necesidad de burlarse a cabo métodos cuando la prueba es muy común y hay un montón de herramientas para ayudarle con él en Python. El peligro con las clases de "parche de monos" como esta es que si no lo hace deshazlo de después, entonces la clase ha sido modificada para todos los demás usos a lo largo de tus pruebas.

Mi biblioteca simulada, que es una de las bibliotecas de burla de Python más populares, incluye un ayudante llamado "parche" que le ayuda a parchear métodos o atributos en objetos y clases durante sus pruebas.

El módulo mock está disponible de:

http://pypi.python.org/pypi/mock

El decorador parche se puede utilizar como un gestor de contexto o como un decorador de prueba. Puede usarlo para parchear con funciones usted mismo, o usarlo para parchear automáticamente con objetos simulados que son muy configurables.

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!" 

Esto se encarga de liberar el parche automáticamente. Se podía escapar sin definir la función de simulacro de sí mismo:

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!" 
+0

Ahh - gracias. Miré 'simulacro', pero no vi ningún ejemplo obvio en el que se utilizara para parchar cosas que se están importando en otro lugar. +1 –

+0

Solo tenga cuidado con el parche de mono directo en sus pruebas, es fácil romper las cosas. mock.patch se puede usar para parchar cosas en cualquier espacio de nombres, ya sea objetos en el espacio de nombres actual (con 'patch.object') o en otro espacio de nombres especificado por el nombre:' patch ('mymodule.myclass') '. – fuzzyman

+3

Lo importante es saber que si está parcheando el nombre, parchee el nombre en el módulo que está * usado *, no en el módulo que está definido. p.ej. si quería parchar el uso de 'SomeClass' en el módulo' foo', incluso cuando 'SomeClass' se define en el módulo' bar' pero se importa en 'foo', entonces lo que se parche es' foo.SomeClass' y * no * 'bar.SomeClass'. Para aplicar parches a un método de clase, no importa tanto como el objeto * de clase será el mismo objeto en ambos módulos. Para otras cosas, importa mucho. – fuzzyman

1

Mock es la manera de hacerlo, está bien. Puede ser un poco complicado asegurarse de que está aplicando parches al método de instancia en cualquier instancia creada a partir de la clase.

# 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 
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!" 

que se hace referencia en el docs, en el párrafo inicial "Para configurar los valores de retorno sobre los métodos de instancias de la clase parcheado ..."

Cuestiones relacionadas