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

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

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 usando side_effect, pero esperaba que hubiera una mejor manera que funcione de la misma manera en todas las funciones, métodos de clase, métodos estáticos, métodos no vinculados, etc.

60
wim 1 sep. 2014 a las 18:33

2 respuestas

La mejor respuesta

Solución similar a la suya, 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)

De acuerdo con la documentación:

envolturas : Elemento para el objeto simulado para envolver. Si wraps no es None, entonces llamar al simulacro pasará la llamada al objeto envuelto (devolviendo el resultado real). El acceso al atributo en el simulacro volverá un objeto simulado que envuelve el atributo correspondiente de la envoltura objeto (por lo que intentar acceder a un atributo que no existe elevar 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)
47
falsetru 1 sep. 2014 a las 14:58

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 de spud.foo. Necesito spud para crearlo con un método simulado foo desde el primer momento, porque no controlo el lugar donde se crea 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 un CopyingMock * en lugar de MagicMock dentro del spy_decorator.

* Es una receta tomada de documentos que publiqué en PyPI como copyingmock lib

12
wim 7 mar. 2019 a las 05:01