Mocking vs patching en Python

En este post veremos qué diferencias hay entre el mocking y el patching que, aunque relacionados, no son lo mismo. También cuando aplica uno y cuando el otro, y algunos de los inconvenientes que pueden llegar a surgir cuando los usemos.

En Python, no es necesario (a priori) instalar ningún paquete para hacer tests. La standard library ya viene de serie con todo lo necesario (framework de testing, test runner e incluso librería para mocking… a esto se refieren cuando dicen que Python es “batteries included”).

# a_test.py
import unittest
from unittest.mock import MagicMock


class FooTestCase(unittest.TestCase):
    def test_foo(self):
        mock_foo = MagicMock(return_value="Foo")
        self.assertEqual("Foo", mock_foo())

Para ejecutar el test bastaría ejecutar el comando python -m unittest a_test.

Este estilo de tests está inspirado en la familia de xUnit y, aunque es perfectamente válido, no es muy pythonico. Por eso, mucha gente opta por usar pytest (ahora sí toca instalar un paquete). Se habla de pruebas funcionales vs pruebas basadas en clases.

#test_foo.py
from unittest.mock import MagicMock


def test_foo():
    mock_foo = MagicMock(return_value="Foo")
    assert "Foo" == mock_foo()

Con pytest test_foo.py podrás ejecutar el test.

Cabe mencionar que pytest (como test runner) puede ejecutar también tests creados con unittest.TestCase, por lo que pueden convivir ambos frameworks en el mismo proyecto sin problema.

Lo que en cualquier caso no cambia es la librería de mocking, que en ambas soluciones usa unittest.mock de la standard library.

En cuanto a las aserciones, puedes usar assert o instalar alguna otra librería de aserciones como assertpy, grappa, expects, sure, etc. También puedes, lógicamente, no instalar ninguna librería adicional y usar los métodos assert* de unittest.TestCase o simplemente la instrucción assert.

#test_foo.py
from unittest.mock import MagicMock
from assertpy import assert_that


def test_foo():
    mock_foo = MagicMock(return_value="Foo")
    assert_that("Foo").is_equal_to(mock_foo())
    # assert "Foo" == mock_foo()

La integración de pytest con VSCode y PyCharm es perfecta y no tendrás que instalar ningún plugin adicional.

Después de esta breve introducción, toca hablar de mocking y patching.

La primera pregunta que podría venirnos a la cabeza es ¿por qué tengo usar mocks?. Tienes que mockear una dependencia de la unidad/SUT que estás probando cuando quieres que se ejercite la unidad de forma aislada, cuando quieres controlar la ejecución del test con respuestas predefinidas que devuelvan lo que tú quieras, cuando te interesa preguntar cuál ha sido el uso que ha tenido la dependencia en el test (esto es si se ha llamado, con qué parámetros, etc.), cuando no quieras usar la implementación real porque tendría un alto coste o directamente es inviable (en términos de tiempo, requisitos de infraestructura, etc.), cuando aun no esté disponible la implementación real (dependes de un componente de terceros que está desarrollándose o estás haciendo TDD outside-in), etc.

En líneas generales, quédate con que un mock es “un objeto simulado que imita el comportamiento de un objeto real de forma controlada”.

Esta explicación da para un post una serie de posts, por lo que te recomiendo leer alguno de los siguientes enlaces donde se cuenta las diferencias que hay entre los distintos dobles de tests: TestDouble, Test Double, Mocks Aren’t Stubs, Mocks Aren’t Stubs (traducción de Carlos Blé del artículo original de Martin Fowler).

En la práctica llamamos mock a cualquier doble de test porque se ha impuesto la palabra mock. Esto es porque muchos frameworks han sobreutilizado la palabra mock. Por ejemplo (y usando ejemplos principalmente de .NET), Moq, Rhino Mocks, FakeItEasy, o incluso el propio unittest.mock de Python. Todos ellos hablan de mocking sin más y, aunque es cierto que hay diferencias entre los distintos tipos de dobles de test, en mi opinión resulta más práctico hablar de mocks que estar discutiendo sobre si el reemplazo es un dummy, un fake, un stub, un spy o un mock. A este respecto me gusta mucho lo que dice NSubstitute “Mock, stub, fake, spy, test double? Strict or loose? Nah, just substitute for the type you need!”

Habiendo explicado (muy por encima) el porqué necesitamos usar mocks y que distintos tipos hay, ¡usémoslos en Python!.

Lo primero que necesitamos es un SUT con algunas dependencias que queramos mockear. Usaremos el manido ejemplo de un servicio y un repositorio.

Aunque se puede usar Mock, en Python se suele usar MagicMock que no es más un Mock con algunos magic methods ya implementados.

import abc
from dataclasses import dataclass
from typing import Iterable

import pyodbc
from assertpy import assert_that


@dataclass
class User:
    id: int
    name: str
    active: bool
    email: str


class UsersRepository(abc.ABC):
    @abc.abstractmethod
    def get_all(self, active: bool) -> Iterable[User]:
        pass


class DatabaseUsersRepository(UsersRepository):
    def __init__(self, connstring: str) -> None:
        self._connstring = connstring

    def get_all(self, active: bool) -> Iterable[User]:
        connection = pyodbc.connect(self._connstring)
        cursor = connection.cursor()
        sql = f"SELECT Id, Name, Email FROM Users WHERE Active = {'1' if active else '0'}"
        cursor.execute(sql)
        row = cursor.fetchone()
        users = []
        while row:
            users.append(User(row[0], row[1], True, row[2]))
            row = cursor.fetchone()
        return users


class EmailSender:
    def send(self, sender: str, recipient: str, body: str) -> None:
        # TODO Send email
        pass


class UsersService:
    def __init__(self, users_repository: UsersRepository, email_sender: EmailSender) -> None:
        self._users_repository = users_repository
        self._email_sender = email_sender

    def send_email_to_users_in_domain(self, recipient: str, domain_to_filter) -> Iterable[str]:
        sent_emails = []
        for user in self._users_repository.get_all(active=True):
            if not user.email.endswith(domain_to_filter):
                continue
            self._email_sender.send(user.email, recipient, f"Hello {user.name} in domain {domain_to_filter}")
            sent_emails.append(user.email)
        return sent_emails


def test_send_email_to_users_in_domain() -> None:
    connstring = f"DRIVER={{ODBC Driver 17 for SQL Server}};SERVER=(local);DATABASE=mocking;UID=sa;PWD=P@ssw0rd;"
    users_repository = DatabaseUsersRepository(connstring)
    email_sender = EmailSender()
    users_service = UsersService(users_repository, email_sender)

    actual = users_service.send_email_to_users_in_domain("no-reply@panicoenlaxbox.com", "gmail.com")

    assert_that(actual).is_equal_to(["panicoenlaxbox@gmail.com"])

En este ejemplo el SUT es el método UsersService.send_email_to_users_in_domain. Sin embargo, para poder ejecutar el test es necesario tener una BD con una tabla Users, donde además tiene que haber un sólo registro con un usuario activo y con el email panicoenlaxbox@gmail.com. Más adelante (cuando presumiblemente esté hecho el #TODO de EmailSender.send), tendríamos que configurar las credenciales para el envío de correo (y cruzar los dedos para que el servicio que estemos usando no esté caído o tenga algún tipo de throttling).

Es obvio que estamos frente a un test de integración de libro. Un test lento y que además parece nos demandará mucho esfuerzo de configuración previa. La idea original del test era probar UsersService.send_email_to_users_in_domain, no UsersRepository ni EmailSender.

Si quisiésemos de verdad ejecutar el test y que fuera FIRST, tendríamos que usar las fixtures de pytest para hacer un setup/cleanup en condiciones (crear la BD, reiniciar el estado de la tabla Users, insertar datos semilla, etc.).

¿Cómo lo hacemos entonces para testear sólo lo que queramos y que las dependencias de nuestro SUT no sean un problema? Ya lo sabes, mockeando.

Y es aquí donde te puedes encontrar (a grandes rasgos) dos distintos escenarios:

  • Si tu código usa inyección de dependencias…, !estás de enhorabuena!. Tú código es testeable y por eso reemplazar las dependencias por un mock debería de ser coser y cantar.
  • Si tu código no usa inyección de dependencias… !monkey patching al rescate!.
    • En Python está muy arraigada esta práctica, pero en otros lenguajes no está tan bien resuelta (por ejemplo en .NET y, aunque hay librerías como Harmony o pose, si puedes evitarlas… mejor).

Veamos en código las diferencias entre hacer mocking y patching.

Primero hagamos que nuestro test sea unitario de verdad usando mocking.

def test_send_email_to_users_in_domain():
    mock_users_repository = MagicMock()  # stub
    mock_users_repository.get_all.return_value = [User(1, "Sergio", True, "panicoenlaxbox@gmail.com")]
    mock_email_sender = MagicMock()  # dummy
    users_service = UsersService(mock_users_repository, mock_email_sender)

    actual = users_service.send_email_to_users_in_domain("no-reply@panicoenlaxbox.com", "gmail.com")

    assert_that(actual).is_equal_to(["panicoenlaxbox@gmail.com"])

easy-peasy! Puesto que UsersService recibía por constructor las dependencias, reemplazarlas ha sido muy fácil.

Hagamos a continuación un pequeño cambio en el código de producción. Básicamente, prescindir de la D de SOLID.

class UsersService:
    def __init__(self) -> None:
        connstring = f"DRIVER={{ODBC Driver 17 for SQL Server}};SERVER=(local);DATABASE=mocking;UID=sa;PWD=P@ssw0rd;"
        self._users_repository = DatabaseUsersRepository(connstring)
        self._email_sender = EmailSender()

Ahora nuestro código ya no es tan testeable. Las dependencias son implícitas y no hay forma sencilla de cambiar el comportamiento de UsersService. Recuerda, new is glue.

Hacer pasar el test es sencillo (siempre y cuando tengas una BD lista y con el estado adecuado)… pero ahora estamos peor que antes, el código de producción es una caja negra y no parece ofrecer ningún punto de extensión.

def test_send_email_to_users_in_domain():
    users_service = UsersService()

    actual = users_service.send_email_to_users_in_domain("no-reply@panicoenlaxbox.com", "gmail.com")

    assert_that(actual).is_equal_to(["panicoenlaxbox@gmail.com"])

Para resolver esta incómoda situación, es donde el patching aparece en escena. La idea es reescribir nuestro código en tiempo de ejecución.

Veamos primero con un ejemplo sencillo (y que no usa la stdlib) que es exactamente el patching.

old_open = open  # save original reference


def custom_open(path, mode):
    print(f"You are opening {path}")  # this is our extra code
    return old_open(path, mode)  # call to the original implementation


open = custom_open  # Monkey patching

with open("C:\\Temp\\foo.txt", "wt") as f:
    f.write("foo")

open = old_open  # restore original reference, unpatch

Ahora usemos ya sí unittest.mock para hacer patching y que nuestro test sea equivalente al test que usaba código de producción que sí era testeable (y era mejor, obvio).

def test_send_email_to_users_in_domain():
    with patch("tests.test_foo.DatabaseUsersRepository") as mock_database_users_repository:
        mock_database_users_repository.return_value = MagicMock(
            get_all=MagicMock(return_value=[User(1, "Sergio", True, "panicoenlaxbox@gmail.com")])
        )
        with patch("tests.test_foo.EmailSender"):
            # In UsersService.__init__, self._email_sender = EmailSender() will create a mock object
            users_service = UsersService()

            actual = users_service.send_email_to_users_in_domain("no-reply@panicoenlaxbox.com", "gmail.com")

            assert_that(actual).is_equal_to(["panicoenlaxbox@gmail.com"])

Es importante no confundir patch del módulo unittest.mock con el fixture monkeypatch de pytest. Además, patch puede ser usado como context manager pero también como decorador.

Como decorador, sería así:

@patch("tests.test_foo.EmailSender")
@patch("tests.test_foo.DatabaseUsersRepository")
def test_send_email_to_users_in_domain(mock_database_users_repository, mock_email_sender):
    mock_database_users_repository.return_value = MagicMock(
        get_all=MagicMock(return_value=[User(1, "Sergio", True, "panicoenlaxbox@gmail.com")])
    )
    users_service = UsersService()

    actual = users_service.send_email_to_users_in_domain("no-reply@panicoenlaxbox.com", "gmail.com")

    assert_that(actual).is_equal_to(["panicoenlaxbox@gmail.com"])

Como puedes ver, hemos hackeado nuestro código (no se me ocurre mejor verbo para ilustrar lo sucedido). Dentro del bloque with, el código del módulo tests.test_foo que use DatabaseUsersRepository usará nuestro mock (porque hacer patch te devuelve un mock) en vez de la implementación real. ¡Salvados por la campana!

El lado oscuro del patching es que debes tener muy claro “que estás parcheando”. La regla de oro es que debes hacer patch “donde se esté usando lo que quieres parchear, no donde esté implementado”, puedes ampliar info aquí “The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined”. Es decir, en nuestro caso sólo tenemos un módulo test_foo.py y allí tenemos tanto el código de producción como el código de test. Por eso el patch es sobre tests.test_foo.DatabaseUsersRepository y todo funciona. Si movemos el código de producción a la raíz del proyecto, ahora el patch dependerá de cómo importemos el módulo users_service.py en el módulo test_foo.py, que es donde se está usando.

from unittest.mock import MagicMock, patch

from assertpy import assert_that

from user import User
import users_service


def test_send_email_to_users_in_domain():
    with patch("tests.test_foo.users_service.DatabaseUsersRepository") as mock_database_users_repository:
        mock_database_users_repository.return_value = MagicMock(get_all=MagicMock(return_value=[
            User(1, "Sergio", True, "panicoenlaxbox@gmail.com")]))
        with patch("tests.test_foo.users_service.EmailSender"):
            users_service_ = users_service.UsersService()

            actual = users_service_.send_email_to_users_in_domain("no-reply@panicoenlaxbox.com", "gmail.com")

            assert_that(actual).is_equal_to(["panicoenlaxbox@gmail.com"])

Fíjate que después de mover todo el código de producción a la raíz del proyecto, hemos tenido que cambiar la forma de importar users_service y también la ruta de lo que queremos patchear, de tests.test_foo.DatabaseUsersRepository a tests.test_foo.users_service.DatabaseUsersRepository. Y si se usara DatabaseUsersRepository en algún otro sitio que no fuera UsersService tendríamos un problema porque eso no estaría parcheado y usaría la implementación original.

Otro problema es el tiempo de aplicación del parche. Cuando hagas un patch también tienes que tener muy claro cuando se hará el unpatch, porque si no tus tests van a depender entre ellos y eso es un problema.

Que puedas hacer algo no significa que debas. Salvo raras excepciones (y es mi opinión), si haces mucho patching es probable que tu diseño no sea el mejor (esto es una forma suave de decir que tu código es… bueno, no testeable es suficiente para que pienses que no está bien).

Algunos casos donde creo el patching puede ser un salvavidas es cuando uses librerías que son un must-know, como por ejemplo:

Con independencia de si usas patching o mocking, algo a tener muy en cuenta es la especificación del mock. En todos nuestros ejemplos (ya sea creando a mano el mock o usando patch), nuestro mock responde a cualquier llamada… es un traga-bolas. Lógicamente eso no está bien porque, fácilmente podría pasar que tuviéramos un test en verde con código de producción que no es correcto, así que la sorpresa vendría después con alevosía y nocturnidad (cuando ya esté publicado el código en producción).

# test code
mock_users_repository = MagicMock()
mock_users_repository.get_all.return_value = [User(1, "Sergio", True, "panicoenlaxbox@gmail.com")]

# production code
for user in self._users_repository.get_all(active=True, a_non_existing_parameter=True):

En este ejemplo, el código de producción llama al método self._users_repository.get_all con el parámetro extra a_non_existing_parameter. El test pasa porque el mock no tiene problema con ello, pero el código de producción fallaría con TypeError: DatabaseUsersRepository.get_all() got an unexpected keyword argument 'a_non_existing_parameter'.

Cierto es que si usas mypy… porque lo usas ¿verdad? te cantaría el error del código de producción, pero…

La conclusión es que mock = MagicMock() no basta y tenemos que ser un poco más concretos a la hora de declarar nuestros mocks.

Trabajaremos sobre este ejemplo para ir refinándolo:

from unittest.mock import MagicMock


class Dog:
    def __init__(self, color: str):
        self.color = color

    def bark(self):
        print(f"A {self.color} dog is barking")


mock_dog = MagicMock()
mock_dog.bark()
mock_dog.bark("slow")  # bark does not receive a str argument, but it works
mock_dog.meow()  # meow works, but it does not exist in Dog class, it shouldn't work
mock_dog.tweet = lambda: print(f"A dog is tweeting")  # We can add new methods to our mock, do we want this?
mock_dog.tweet()

En una primera aproximación podríamos usar spec, spec_set y seal.

Con spec podemos pasarle una lista de strings, una clase o un objeto. Si es clase u objeto se llamará a dir sobre el mismo. En cualquier caso, lo que estamos haciendo es crear una especificación en el mock, por lo que si llamamos a un método que no existe, el mock fallará con AttributeError. Como contrapartida, spec no valida parámetros y podemos seguir añadiendo métodos al mock.

print([attr for attr in dir(Dog) if not attr.startswith("__")])  # ['bark']

my_dog = Dog("brown")
print([attr for attr in dir(my_dog) if not attr.startswith("__")])  # ['bark', 'color']

mock_dog = MagicMock(spec=Dog)
assert isinstance(mock_dog, Dog)
mock_dog.bark()
# print(mock_dog.color)  # Only if we used my_dog as spec
mock_dog.bark("slow")  # 😒, bark with parameter continues working
# mock_dog.meow() # AttributeError: Mock object has no attribute 'meow'
mock_dog.tweet = lambda: print(f"A dog is tweeting")
mock_dog.tweet()

La variante con una lista de strings como spec también es válida:

mock_dog = MagicMock(spec=["bark", "color"])
assert not isinstance(mock_dog, Dog)  # Now, mock_dog is not a Dog instance
mock_dog.bark()
print(mock_dog.color)
mock_dog.bark("slow")  # 😒, bark with parameter continues working
# mock_dog.meow() # AttributeError: Mock object has no attribute 'meow'
mock_dog.tweet = lambda: print(f"A dog is tweeting")
mock_dog.tweet()

Con spec_set (una variante de spec) subimos un poco el nivel y ahora no podremos agregar métodos al mock, porque digo yo… no queremos inventarnos métodos ¿no?.

mock_dog = MagicMock(spec_set=Dog)
mock_dog.bark()
mock_dog.bark("slow")  # 😒, bark with parameter continues working
# mock_dog.meow() # AttributeError: Mock object has no attribute 'meow'
# mock_dog.tweet = lambda: print(f"A dog is tweeting")  # AttributeError: Mock object has no attribute 'tweet'
# mock_dog.tweet()

Con seal, nos ponemos un poco más hardcore y sólo podremos llamar a métodos que hayan sido configurados previamente. Además también resolvemos que los parámetros suministrados no coincidan con la firma real del método.

mock_dog = MagicMock(spec_set=Dog)
seal(mock_dog)
mock_dog.bark()  # AttributeError: mock.bark

Toca configuarar el mock toca previamente:

mock_dog = MagicMock(spec_set=Dog)
mock_dog.bark.side_effect = lambda: print("Barking from a mock")
seal(mock_dog)
mock_dog.bark()
# mock_dog.bark("slow")  # TypeError: <lambda>() takes 0 positional arguments but 1 was given

El constructor de MagicMock recibe como último parámetro **kwargs lo que permite crearlo y configurarlo en un sólo paso:

mock_dog = MagicMock(spec_set=Dog)
mock_dog.bark.side_effect = lambda: print("Barking from a mock")
# a short version of the above
mock_dog2 = MagicMock(spec_set=Dog, **{"bark.side_effect": lambda: print("Barking from a mock")})

En este punto, mi recomendación es usar spec_set y seal. Ahora sí, tenemos un Mock confiable y que sustituye a la dependencia real con garantías.

Algo que seguro te dará guerra (y que hemos estando obviando hasta ahora) son las propiedades de instancia creadas en el constructor inicializador.

from unittest.mock import MagicMock, seal


class Dog:
    def __init__(self, color: str):
        self.color = color

    def bark(self):
        print(f"A {self.color} dog is barking")


mock_dog = MagicMock(spec_set=Dog)
seal(mock_dog)
print(mock_dog.color)  # AttributeError: Mock object has no attribute 'color'

Esto sucede porque MagicMock no ejecuta el código de __init__, luego no sabe que existe la propiedad color.

Houston, tenemos un problema ¿Qué podemos hacer?

  • Añadir estas propiedades a mano en el mock… pero entonces no podríamos usar spec_set.
  • Pasar las variables de instancia a variables de clase…… no es una solución, es otro diseño, otra aplicación.
  • Crear el mock a partir de una instancia de objeto o de una lista de strings (no de una clase).
  • Pasar los atributos de instancia a @property y usar PropertyMock en la configuración del mock.

Si usamos una lista de strings el problema es que estamos hardcodeando strings y eso puede llevarnos a errores, pero bueno…

mock_dog = MagicMock(spec_set=['bark', 'color'])
mock_dog.bark.side_effect = lambda: print(f"A {mock_dog.color} dog is barking")
mock_dog.color = "brown"
seal(mock_dog)
mock_dog.bark()  # A brown dog is barking

Además no funcionará isintance:

mock_dog = MagicMock(spec_set=['bark', 'color'])
seal(mock_dog)
assert isinstance(mock_dog, Dog)

Aunque podemos solucionarlo:

mock_dog = MagicMock(spec_set=['bark', 'color', '__class__'])
mock_dog.__class__ = Dog
seal(mock_dog)
assert isinstance(mock_dog, Dog)

Si pasamos las propiedades de instancia a @property (que también te digo no apetece hacer esto porque nos obligue a ello el Mock, sino hacerlo porque realmente lo querías acorde a tu diseño…):

from unittest.mock import MagicMock, seal, PropertyMock


from unittest.mock import MagicMock, PropertyMock, seal


class Dog:
    def __init__(self, color: str):
        self.color = color

    def bark(self):
        print(f"A {self.color} dog is barking")

    @property
    def color(self) -> str:
        return self._color

    @color.setter
    def color(self, value: str) -> None:
        self._color = value


mock_dog = MagicMock(spec_set=Dog)
# https://docs.python.org/3/library/unittest.mock.html#unittest.mock.PropertyMock
type(mock_dog).color = PropertyMock(return_value="brown")
seal(mock_dog)
mock_dog.color = "white"  # Let's image that this code is production code...
print(mock_dog.color)  # because PropertyMock was configured to return "black", it will return "black" instead of "white"

En este último ejemplo también hemos visto la diferencia entre usar mock_dog.color="brown" o type(mock_dog).color = PropertyMock(return_value="brown") cuando estamos configurando el Mock.

En cualquier caso, hay que reconocer que se está complicando crear el mock definitivo.

Como alternativa al constructor de MagicMock podemos usar autospeccing.

from unittest.mock import PropertyMock, create_autospec, seal


class Dog:
    def __init__(self, color: str):
        self.color = color

    def bark(self):
        print(f"A {self.color} dog is barking")

    @property
    def color(self):
        return self._color

    @color.setter
    def color(self, value):
        self._color = value


mock_dog = create_autospec(spec=Dog, spec_set=True)
mock_dog.bark.side_effect = lambda: print(f"A {mock_dog.color} dog is barking")
type(mock_dog).color = PropertyMock(return_value="brown")
seal(mock_dog)
mock_dog.bark()  # A brown dog is barking

Finalmente, si no tienes que lidiar con propiedades (o ya tienes @property) te recomendaria create_autospec y seal. En caso contrario, lo más sensato parece usar spect_set con una lista de strings y seal. No parece haber solución perfecta (o yo no la he encontrado… que será más probable).

Por más digno que me ponga en este post, tengo que reconocer que muchas veces me seduce la simpleza de mock = MagicMock() y termino relajando la especificación del mock en aras de simplificar el setup del test. Ahora bien, estoy añadiendo un punto de fallo a mis tests y comprando deliberadamente un riesgo.

Por ejemplo, que fácil es hacer lo siguiente:

mock_foo = MagicMock()
mock_foo.bar = MagicMock()
mock_foo.bar.baz = MagicMock()
mock_foo.bar.baz.qux = "quux"
print(mock_foo.bar.baz.qux)  # "quux"

# Equivalent to the above, more compact
mock_foo2 = MagicMock(**{"bar.baz.qux": "quux"})
print(mock_foo2.bar.baz.qux)  # "quux"

Por último, y aunque es un caso más excepcional, puede ser que ya tengas un objeto y quieras mockear sólo parte de su interfaz y para el resto llamar a la implementación real. Puedes hacerlo con el parámetro wraps.

from unittest.mock import MagicMock


class Dog:
    def __init__(self, color: str):
        self.color = color

    def bark(self):
        print(f"A {self.color} dog is barking")

    def walk(self):
        print(f"A {self.color} dog is walking")


dog = Dog("brown")
mock_dog = MagicMock(wraps=dog)
mock_dog.bark()  # It will call the original method
mock_dog.walk.side_effect = lambda: print("I don't want to walk")
mock_dog.walk()
# We spy on the dog instance
mock_dog.bark.assert_called_once()
mock_dog.walk.assert_called_once()

¡Un saludo!


Ver también