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.
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
Mock vs Stub
Ejemplos del Mundo Real
Casos de Uso
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.
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()
---------------------------------------------------------
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
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
Casos de Uso
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.
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"))
Unit Test con Mock Inyectado
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()
---------------------------------------------------------
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.
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!")
Unit Testing con Stubs
Para los tests, usaremos stubs que implementan la interfaz y registran las llamadas para verificación.
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()
---------------------------------------------------------
Ran 2 tests in 0.000s
OK
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.