Una función o clase está muy acoplada cuando crea por dentro los objetos concretos que necesita para trabajar. Ese diseño dificulta las pruebas, porque obliga a usar archivos, bases de datos, correo, reloj real o servicios externos aunque solo queramos verificar una regla de negocio.
En este tema aplicaremos inyección de dependencias: pasaremos desde afuera los colaboradores que el código necesita. Así podremos reemplazar infraestructura real por objetos simples durante las pruebas.
Una dependencia es cualquier elemento externo que una unidad de código necesita para cumplir su trabajo: un repositorio, un cliente HTTP, un enviador de correos, una función que obtiene la fecha actual o un generador de identificadores.
El problema no es tener dependencias. El problema aparece cuando el código las construye directamente y no permite reemplazarlas.
def procesar_pago(pago):
cliente = ClienteCorreoSMTP()
reloj = RelojDelSistema()
repositorio = RepositorioPagosSQLite()
# regla de negocio mezclada con infraestructura
Crea el archivo src/recibos.py con una implementación deliberadamente acoplada:
from datetime import datetime
class RepositorioRecibos:
def guardar(self, recibo):
print(f"Guardando recibo {recibo['numero']}")
class EnviadorCorreo:
def enviar(self, destino, asunto, cuerpo):
print(f"Enviando correo a {destino}: {asunto}")
def emitir_recibo(pago):
repositorio = RepositorioRecibos()
correo = EnviadorCorreo()
fecha = datetime.now().date().isoformat()
recibo = {
"numero": f"REC-{pago['id']}",
"cliente": pago["cliente"],
"email": pago["email"],
"total": pago["total"],
"fecha": fecha,
}
repositorio.guardar(recibo)
correo.enviar(
pago["email"],
"Recibo emitido",
f"Recibimos tu pago por {pago['total']}",
)
return recibo
La función mezcla la construcción del recibo, la fecha actual, la persistencia y el envío de correo.
La regla principal debería ser simple: dado un pago, se genera un recibo con número, cliente, total y fecha. Sin embargo, la función usa la fecha real y ejecuta efectos secundarios.
Podemos escribir una prueba, pero queda frágil porque depende del día en que se ejecuta:
from src.recibos import emitir_recibo
def test_emite_recibo_con_datos_del_pago():
pago = {
"id": 15,
"cliente": "Ana",
"email": "ana@example.com",
"total": 12000,
}
recibo = emitir_recibo(pago)
assert recibo["numero"] == "REC-15"
assert recibo["cliente"] == "Ana"
assert recibo["total"] == 12000
python -m pytest tests/test_recibos.py
Antes de refactorizar, conviene señalar qué cosas queremos controlar desde afuera:
Estos tres elementos son colaboradores. La función no necesita saber cómo se construyen; solo necesita usarlos.
El primer paso seguro es separar la creación del recibo. Esta función no guarda, no envía correo y no lee el reloj del sistema:
def construir_recibo(pago, fecha):
return {
"numero": f"REC-{pago['id']}",
"cliente": pago["cliente"],
"email": pago["email"],
"total": pago["total"],
"fecha": fecha,
}
Ahora podemos probar la parte más importante sin infraestructura:
from src.recibos import construir_recibo
def test_construye_recibo_con_fecha_recibida():
pago = {"id": 15, "cliente": "Ana", "email": "ana@example.com", "total": 12000}
recibo = construir_recibo(pago, "2026-05-11")
assert recibo == {
"numero": "REC-15",
"cliente": "Ana",
"email": "ana@example.com",
"total": 12000,
"fecha": "2026-05-11",
}
Una forma simple de reducir acoplamiento es pasar una función que devuelva la fecha actual. En producción usaremos una función real; en pruebas, una función fija.
from datetime import datetime
def fecha_actual():
return datetime.now().date().isoformat()
def emitir_recibo(pago, obtener_fecha=fecha_actual):
fecha = obtener_fecha()
recibo = construir_recibo(pago, fecha)
# todavía falta resolver repositorio y correo
return recibo
La dependencia ya no está escondida dentro de la función: ahora se puede reemplazar.
Para la prueba no necesitamos modificar el reloj del sistema. Pasamos una función común que devuelve siempre la misma fecha:
from src.recibos import emitir_recibo
def test_emite_recibo_con_fecha_inyectada():
pago = {"id": 15, "cliente": "Ana", "email": "ana@example.com", "total": 12000}
recibo = emitir_recibo(pago, obtener_fecha=lambda: "2026-05-11")
assert recibo["fecha"] == "2026-05-11"
python -m pytest tests/test_recibos.py
Ahora pasamos también el repositorio y el enviador de correo. La función deja de crear clases concretas:
def emitir_recibo(pago, repositorio, correo, obtener_fecha=fecha_actual):
recibo = construir_recibo(pago, obtener_fecha())
repositorio.guardar(recibo)
correo.enviar(
pago["email"],
"Recibo emitido",
f"Recibimos tu pago por {pago['total']}",
)
return recibo
La función conserva el comportamiento, pero ahora recibe sus colaboradores desde afuera.
Podemos crear objetos simples que registren qué ocurrió. No necesitan conectarse a nada real:
class RepositorioFalso:
def __init__(self):
self.recibos = []
def guardar(self, recibo):
self.recibos.append(recibo)
class CorreoFalso:
def __init__(self):
self.mensajes = []
def enviar(self, destino, asunto, cuerpo):
self.mensajes.append({
"destino": destino,
"asunto": asunto,
"cuerpo": cuerpo,
})
Estos objetos permiten verificar efectos secundarios sin depender de infraestructura.
Con dependencias inyectadas podemos verificar que se guardó el recibo y que se intentó enviar el correo correcto:
from src.recibos import emitir_recibo
def test_guarda_y_envia_recibo():
repositorio = RepositorioFalso()
correo = CorreoFalso()
pago = {"id": 15, "cliente": "Ana", "email": "ana@example.com", "total": 12000}
recibo = emitir_recibo(
pago,
repositorio=repositorio,
correo=correo,
obtener_fecha=lambda: "2026-05-11",
)
assert repositorio.recibos == [recibo]
assert correo.mensajes == [{
"destino": "ana@example.com",
"asunto": "Recibo emitido",
"cuerpo": "Recibimos tu pago por 12000",
}]
python -m pytest tests/test_recibos.py
El módulo queda más explícito. Las clases reales siguen existiendo, pero ya no están escondidas dentro de la función principal:
from datetime import datetime
class RepositorioRecibos:
def guardar(self, recibo):
print(f"Guardando recibo {recibo['numero']}")
class EnviadorCorreo:
def enviar(self, destino, asunto, cuerpo):
print(f"Enviando correo a {destino}: {asunto}")
def fecha_actual():
return datetime.now().date().isoformat()
def construir_recibo(pago, fecha):
return {
"numero": f"REC-{pago['id']}",
"cliente": pago["cliente"],
"email": pago["email"],
"total": pago["total"],
"fecha": fecha,
}
def emitir_recibo(pago, repositorio, correo, obtener_fecha=fecha_actual):
recibo = construir_recibo(pago, obtener_fecha())
repositorio.guardar(recibo)
correo.enviar(
pago["email"],
"Recibo emitido",
f"Recibimos tu pago por {pago['total']}",
)
return recibo
Si no queremos que toda la aplicación conozca las clases reales, podemos crear una función pequeña que arme el caso de uso de producción:
def emitir_recibo_en_produccion(pago):
return emitir_recibo(
pago,
repositorio=RepositorioRecibos(),
correo=EnviadorCorreo(),
)
La composición queda en el borde del sistema. El núcleo sigue siendo fácil de probar.
Con Protocol podemos expresar qué métodos necesita el caso de uso sin obligar a heredar de una clase base:
from typing import Protocol
class RepositorioDeRecibos(Protocol):
def guardar(self, recibo: dict) -> None:
...
class ServicioDeCorreo(Protocol):
def enviar(self, destino: str, asunto: str, cuerpo: str) -> None:
...
def emitir_recibo(
pago: dict,
repositorio: RepositorioDeRecibos,
correo: ServicioDeCorreo,
obtener_fecha=fecha_actual,
) -> dict:
recibo = construir_recibo(pago, obtener_fecha())
repositorio.guardar(recibo)
correo.enviar(pago["email"], "Recibo emitido", f"Recibimos tu pago por {pago['total']}")
return recibo
El beneficio principal es comunicativo: cualquier objeto que tenga esos métodos puede ser usado.
La inyección de dependencias es especialmente útil cuando una unidad de código:
No todas las funciones necesitan recibir diez dependencias. Si una función pura calcula un impuesto o formatea un texto, agregar objetos y configuraciones puede hacerla menos clara.
Una señal práctica: si las pruebas ya son simples, rápidas y expresivas, quizá no necesitas más inyección. Aplica este refactoring cuando reduzca fricción real.
Refactoriza esta función para que el almacenamiento y la generación de identificadores se puedan reemplazar en pruebas:
from uuid import uuid4
class RepositorioTickets:
def guardar(self, ticket):
print(f"Ticket guardado: {ticket['id']}")
def crear_ticket(usuario, descripcion):
ticket = {
"id": str(uuid4()),
"usuario": usuario,
"descripcion": descripcion,
"estado": "abierto",
}
repositorio = RepositorioTickets()
repositorio.guardar(ticket)
return ticket
Objetivo: poder probar el identificador y el guardado sin usar uuid4 real ni repositorio real.
from uuid import uuid4
class RepositorioTickets:
def guardar(self, ticket):
print(f"Ticket guardado: {ticket['id']}")
def generar_id():
return str(uuid4())
def construir_ticket(usuario, descripcion, id_ticket):
return {
"id": id_ticket,
"usuario": usuario,
"descripcion": descripcion,
"estado": "abierto",
}
def crear_ticket(usuario, descripcion, repositorio, generar_id_ticket=generar_id):
ticket = construir_ticket(usuario, descripcion, generar_id_ticket())
repositorio.guardar(ticket)
return ticket
Prueba posible:
from src.tickets import crear_ticket
class RepositorioTicketsFalso:
def __init__(self):
self.tickets = []
def guardar(self, ticket):
self.tickets.append(ticket)
def test_crea_y_guarda_ticket_con_id_inyectado():
repositorio = RepositorioTicketsFalso()
ticket = crear_ticket(
"ana",
"No puedo ingresar",
repositorio=repositorio,
generar_id_ticket=lambda: "ticket-123",
)
assert ticket == {
"id": "ticket-123",
"usuario": "ana",
"descripcion": "No puedo ingresar",
"estado": "abierto",
}
assert repositorio.tickets == [ticket]
python -m pytest tests/test_tickets.py
Antes de continuar, verifica que puedes explicar estos puntos:
En este tema reducimos acoplamiento mediante inyección de dependencias. El código dejó de crear colaboradores concretos en el centro de la lógica y empezó a recibirlos desde afuera.
En el próximo tema separaremos reglas de negocio, entrada, salida y persistencia.