sábado, 17 de enero de 2026

Principios Fundamentales de Diseño en Python - Parte 10 - Patrones para Testing

Patrones para Testing en Python
Python // Design Patterns

Patrones para Testing

Estrategias para escribir tests confiables, aislados y mantenibles

Los patrones de testing son fundamentales para crear código de alta calidad. Permiten aislar componentes durante las pruebas, hacer los tests más confiables y promover la reutilización del código.

En este artículo exploraremos dos patrones esenciales: el Mock Object Pattern para simular comportamientos y verificar interacciones, y el Dependency Injection Pattern para crear código flexible y testeable.

PATTERN_01

Mock Object Pattern

El patrón Mock Object es una herramienta poderosa para aislar componentes durante el testing simulando su comportamiento. Los objetos mock ayudan a crear entornos de prueba controlados y verificar interacciones entre componentes.

Características Principales

01
AISLAMIENTO
Los mocks aíslan la unidad de código bajo prueba en un entorno controlado
02
VERIFICACIÓN
Permiten verificar que ciertos comportamientos ocurran durante el test
03
SIMPLIFICACIÓN
Simplifican el setup reemplazando objetos complejos

Mock vs Stub

Mock Objects
Reemplazan implementaciones reales y además verifican interacciones: llamadas a métodos, argumentos, orden de ejecución. Son más flexibles para testing comportamental.
Stubs
Solo proveen input indirecto al código bajo prueba. Retornan valores predefinidos pero no verifican cómo fueron llamados.

Ejemplos del Mundo Real

✈️
SIMULADOR DE VUELO
Replica la experiencia de volar sin un avión real
🫀
MANIQUÍ DE CPR
Simula el cuerpo humano para entrenamiento médico
🚗
CRASH TEST DUMMY
Simula reacciones humanas en colisiones vehiculares

Casos de Uso

UNIT TESTING
Reemplazar dependencias complejas o no disponibles del código bajo prueba
INTEGRATION TESTING
Simular componentes en desarrollo o costosos de involucrar
BEHAVIOR VERIFICATION
Verificar interacciones específicas entre objetos
Flujo del Mock Object Pattern
Test Case
―― creates ――
Mock Object
―― replaces ――
Real Dependency

Implementación con unittest.mock

Imaginemos que tenemos una función que escribe mensajes a un archivo de log. Podemos mockear el mecanismo de escritura para asegurar que nuestra función escribe el contenido esperado sin crear archivos reales.

mock_object.py
import unittest
from unittest.mock import mock_open, patch

class Logger:
    def __init__(self, filepath):
        self.filepath = filepath
    
    def log(self, message):
        with open(self.filepath, "a") as file:
            file.write(f"{message}\n")

class TestLogger(unittest.TestCase):
    def test_log(self):
        msg = "Hello, logging world!"
        
        # Crear mock para la función open()
        m_open = mock_open()
        
        # Parchear builtins.open con nuestro mock
        with patch("builtins.open", m_open):
            logger = Logger("dummy.log")
            logger.log(msg)
        
        # Verificar que open() fue llamado correctamente
        m_open.assert_called_once_with("dummy.log", "a")
        
        # Verificar que write() fue llamado con el mensaje
        m_open().write.assert_called_once_with(f"{msg}\n")

if __name__ == "__main__":
    unittest.main()
python mock_object.py
.
---------------------------------------------------------
Ran 1 test in 0.012s

OK
mock_open() retorna un Mock configurado para comportarse como open(). El decorador patch() reemplaza temporalmente el objeto target con el mock durante el scope del test.

Componentes Clave de unittest.mock

unittest.mock Components
patch()
mock_open()
MagicMock
↓ provides ↓
assert_called_once_with()
return_value
side_effect
PATCH
Reemplaza objetos con mocks durante el testing
RETURN_VALUE
Define respuestas fijas del mock
SIDE_EFFECT
Define comportamiento condicional o excepciones
PATTERN_02

Dependency Injection Pattern

El patrón Dependency Injection (DI) consiste en pasar las dependencias de una clase como entidades externas en lugar de crearlas dentro de la clase. Esto promueve el acoplamiento débil, la modularidad y la testabilidad.

Ejemplos del Mundo Real

🔌
ENCHUFES ELÉCTRICOS
Los aparatos se conectan a diferentes tomas sin cableado permanente
📷
LENTES DE CÁMARA
El fotógrafo cambia lentes según las necesidades sin cambiar la cámara
🚂
TRENES MODULARES
Los vagones se añaden o quitan según las necesidades del viaje

Casos de Uso

DATABASE CONNECTIONS
Inyectar conexiones permite cambiar entre motores de BD fácilmente
CONFIGURATION
Gestionar settings entre ambientes (dev, test, prod) dinámicamente
UNIT TESTING
Inyectar mocks para probar componentes en aislamiento
Dependency Injection Flow
WeatherService
← inject →
RealApiClient
MockApiClient

Implementación con Mock Object

Crearemos un escenario donde WeatherService depende de un WeatherApiClient para obtener datos del clima. En el test, inyectaremos una versión mock del cliente.

di_with_mock.py
from typing import Protocol

# Definir la interfaz que cualquier cliente debe cumplir
class WeatherApiClient(Protocol):
    def fetch_weather(self, location):
        """Fetch weather data for a given location"""
        ...

# Implementación real del cliente
class RealWeatherApiClient:
    def fetch_weather(self, location):
        return f"Real weather data for {location}"

# Servicio que usa inyección de dependencias
class WeatherService:
    def __init__(self, weather_api: WeatherApiClient):
        self.weather_api = weather_api
    
    def get_weather(self, location):
        return self.weather_api.fetch_weather(location)

# Uso con la implementación real
if __name__ == "__main__":
    ws = WeatherService(RealWeatherApiClient())
    print(ws.get_weather("Paris"))
python di_with_mock.py
Real weather data for Paris

Unit Test con Mock Inyectado

test_di_with_mock.py
import unittest
from di_with_mock import WeatherService

# Mock del cliente API para testing
class MockWeatherApiClient:
    def fetch_weather(self, location):
        return f"Mock weather data for {location}"

class TestWeatherService(unittest.TestCase):
    def test_get_weather(self):
        # Inyectar el mock en lugar del cliente real
        mock_api = MockWeatherApiClient()
        weather_service = WeatherService(mock_api)
        
        # Verificar el comportamiento
        self.assertEqual(
            weather_service.get_weather("Anywhere"),
            "Mock weather data for Anywhere"
        )

if __name__ == "__main__":
    unittest.main()
python test_di_with_mock.py
.
---------------------------------------------------------
Ran 1 test in 0.000s

OK
WeatherService no necesita saber si está usando un cliente real o un mock, haciendo el sistema más modular y fácil de testear. La dependencia se inyecta a través del constructor.

Implementación con Decoradores

También es posible usar decoradores para DI, lo que simplifica el proceso de inyección. Crearemos un sistema de notificaciones que puede enviar mensajes por diferentes canales.

Decorator-based Dependency Injection
@inject_sender
―― decorates ――
NotificationService
―― injects ――
EmailSender
SMSSender
di_with_decorator.py
from typing import Protocol

# Interfaz para los senders
class NotificationSender(Protocol):
    def send(self, message: str):
        """Send a notification with the given message"""
        ...

# Implementaciones concretas
class EmailSender:
    def send(self, message: str):
        print(f"Sending Email: {message}")

class SMSSender:
    def send(self, message: str):
        print(f"Sending SMS: {message}")

# Decorador para inyección de dependencias
def inject_sender(sender_cls):
    def decorator(cls):
        cls.sender = sender_cls()
        return cls
    return decorator

# Servicio decorado con el sender inyectado
@inject_sender(EmailSender)
class NotificationService:
    sender: NotificationSender = None
    
    def notify(self, message):
        self.sender.send(message)

# Uso
if __name__ == "__main__":
    service = NotificationService()
    service.notify("Hello, this is a test notification!")
python di_with_decorator.py
Sending Email: Hello, this is a test notification!

Unit Testing con Stubs

Para los tests, usaremos stubs que implementan la interfaz y registran las llamadas para verificación.

test_di_with_decorator.py
import unittest
from di_with_decorator import (
    NotificationSender,
    NotificationService,
    inject_sender,
)

# Stubs para testing
class EmailSenderStub:
    def __init__(self):
        self.messages_sent = []
    
    def send(self, message: str):
        self.messages_sent.append(message)

class SMSSenderStub:
    def __init__(self):
        self.messages_sent = []
    
    def send(self, message: str):
        self.messages_sent.append(message)

class TestNotifService(unittest.TestCase):
    def test_notify_with_email(self):
        email_stub = EmailSenderStub()
        service = NotificationService()
        service.sender = email_stub
        
        service.notify("Test Email Message")
        
        self.assertIn(
            "Test Email Message",
            email_stub.messages_sent
        )
    
    def test_notify_with_sms(self):
        sms_stub = SMSSenderStub()
        
        # Crear servicio personalizado con SMS inyectado
        @inject_sender(SMSSenderStub)
        class CustomNotificationService:
            sender: NotificationSender = None
            
            def notify(self, message):
                self.sender.send(message)
        
        service = CustomNotificationService()
        service.sender = sms_stub
        service.notify("Test SMS Message")
        
        self.assertIn(
            "Test SMS Message",
            sms_stub.messages_sent
        )

if __name__ == "__main__":
    unittest.main()
python test_di_with_decorator.py
..
---------------------------------------------------------
Ran 2 tests in 0.000s

OK
Usar decoradores para gestionar dependencias permite cambios fáciles sin modificar las clases internamente. Esto mantiene la aplicación flexible y encapsula la gestión de dependencias fuera de la lógica de negocio.

Resumen de Patrones

01 // Mock Object Pattern

Aísla componentes durante testing simulando comportamientos. Permite verificar interacciones y simplifica el setup de tests.

02 // Dependency Injection

Pasa dependencias como entidades externas promoviendo acoplamiento débil, modularidad y testabilidad.

03 // unittest.mock

Módulo de Python que provee patch(), mock_open() y verificación de llamadas para testing efectivo.

04 // Stubs vs Mocks

Los stubs proveen input indirecto, los mocks además verifican interacciones entre componentes.

Basado en "Mastering Python Design Patterns" // Kamon Ayeva & Sakis Kasampalis