simulacro de python – parcheando un método sin obstruir la implementación

¿Existe una forma limpia de parchear un objeto para que obtenga los ayudantes assert_call* en su caso de prueba, sin eliminar realmente la acción?

Por ejemplo, ¿cómo puedo modificar la línea @patch para obtener el siguiente paso de prueba:

 from unittest import TestCase from mock import patch class Potato(object): def foo(self, n): return self.bar(n) def bar(self, n): return n + 2 class PotatoTest(TestCase): @patch.object(Potato, 'foo') def test_something(self, mock): spud = Potato() forty_two = spud.foo(n=40) mock.assert_called_once_with(n=40) self.assertEqual(forty_two, 42) 

Probablemente podría hackear esto junto con side_effect , pero esperaba que hubiera una forma más agradable que funcionara de la misma manera en todas las funciones, métodos de clase, métodos estáticos, métodos no vinculados, etc.

Solución similar a la tuya, pero usando wraps :

 def test_something(self): spud = Potato() with patch.object(Potato, 'foo', wraps=spud.foo) as mock: forty_two = spud.foo(n=40) mock.assert_called_once_with(n=40) self.assertEqual(forty_two, 42) 

Según la documentación :

envolturas : Elemento para el objeto simulado para envolver. Si wraps no es Ninguno, la llamada al Mock pasará la llamada al objeto envuelto (devolviendo el resultado real). El acceso de atributo en el simulacro devolverá un objeto simulado que envuelve el atributo correspondiente del objeto envuelto (por lo tanto, intentar acceder a un atributo que no existe generará un AttributeError).


 class Potato(object): def spam(self, n): return self.foo(n=n) def foo(self, n): return self.bar(n) def bar(self, n): return n + 2 class PotatoTest(TestCase): def test_something(self): spud = Potato() with patch.object(Potato, 'foo', wraps=spud.foo) as mock: forty_two = spud.spam(n=40) mock.assert_called_once_with(n=40) self.assertEqual(forty_two, 42) 

Esta respuesta aborda el requisito adicional mencionado en la recompensa del usuario Quuxplusone:

Lo importante para mi caso de uso es que funciona con @patch.mock , es decir, que no requiere que inserte ningún código entre mi construcción de la instancia de Potato ( spud en este ejemplo) y mi llamado a spud.foo . Necesito que se cree el spud con un método foo desde el principio, porque no controlo el lugar donde se crea el spud .

El caso de uso descrito anteriormente podría lograrse sin demasiados problemas utilizando un decorador:

 import unittest import unittest.mock # Python 3 def spy_decorator(method_to_decorate): mock = unittest.mock.MagicMock() def wrapper(self, *args, **kwargs): mock(*args, **kwargs) return method_to_decorate(self, *args, **kwargs) wrapper.mock = mock return wrapper def spam(n=42): spud = Potato() return spud.foo(n=n) class Potato(object): def foo(self, n): return self.bar(n) def bar(self, n): return n + 2 class PotatoTest(unittest.TestCase): def test_something(self): foo = spy_decorator(Potato.foo) with unittest.mock.patch.object(Potato, 'foo', foo): forty_two = spam(n=40) foo.mock.assert_called_once_with(n=40) self.assertEqual(forty_two, 42) if __name__ == '__main__': unittest.main() 

Si el método reemplazado acepta argumentos mutables que se modifican bajo prueba, es posible que desee inicializar CopyingMock * en lugar de MagicMock dentro del spy_decorator.

* Es una receta tomada de los documentos que he publicado en PyPI como copyingmock lib