En los temas anteriores vimos dobles de prueba como stubs, mocks y fakes. Para poder usarlos, la unidad debe permitir reemplazar sus dependencias reales por dependencias controladas.
La inyección de dependencias consiste en recibir una dependencia desde afuera en lugar de crearla internamente. Esto facilita las pruebas unitarias porque permite pasar un doble de prueba durante el test.
En este tema veremos la idea de forma práctica, con ejemplos simples y sin entrar en frameworks avanzados.
Una dependencia es algo que una unidad necesita para realizar su trabajo. Puede ser un objeto, función, servicio, configuración o repositorio.
Ejemplos:
Si la unidad crea esas dependencias por su cuenta, probarla de forma aislada puede volverse difícil.
Veamos una función que crea internamente el servicio que necesita.
class ServicioCotizacionReal:
def obtener_cotizacion(self):
# Consulta una API externa
...
def convertir_a_dolares(monto):
servicio = ServicioCotizacionReal()
cotizacion = servicio.obtener_cotizacion()
return monto / cotizacion
Esta función es difícil de probar unitariamente porque siempre usa el servicio real. No podemos pasar fácilmente una cotización controlada.
Podemos modificar la función para recibir el servicio desde afuera.
def convertir_a_dolares(monto, servicio_cotizacion):
cotizacion = servicio_cotizacion.obtener_cotizacion()
return monto / cotizacion
Ahora la función no decide qué servicio usar. Recibe una dependencia y la utiliza. En producción podemos pasar el servicio real; en pruebas podemos pasar un stub.
class CotizacionStub:
def obtener_cotizacion(self):
return 250
def test_convertir_a_dolares():
servicio = CotizacionStub()
resultado = convertir_a_dolares(1000, servicio)
assert resultado == 4
La inyección de dependencias permite reemplazar la API real por una respuesta controlada.
La forma más simple de inyección es pasar la dependencia como parámetro de una función.
def mostrar_precio(precio, configuracion):
moneda = configuracion.obtener_moneda()
return f"{moneda} {precio}"
En la prueba, podemos pasar una configuración controlada:
class ConfiguracionStub:
def obtener_moneda(self):
return "USD"
def test_mostrar_precio_en_dolares():
configuracion = ConfiguracionStub()
assert mostrar_precio(100, configuracion) == "USD 100"
En clases, una forma común es recibir dependencias en el constructor.
class ServicioPedidos:
def __init__(self, repositorio, notificador):
self.repositorio = repositorio
self.notificador = notificador
def aprobar(self, pedido):
pedido["estado"] = "aprobado"
self.repositorio.guardar(pedido)
self.notificador.enviar("Pedido aprobado")
La clase no crea el repositorio ni el notificador. Los recibe. Eso permite usar dobles en las pruebas.
class RepositorioFake:
def __init__(self):
self.pedidos = []
def guardar(self, pedido):
self.pedidos.append(pedido)
class NotificadorMock:
def __init__(self):
self.mensajes = []
def enviar(self, mensaje):
self.mensajes.append(mensaje)
def test_aprobar_pedido_guarda_y_notifica():
repositorio = RepositorioFake()
notificador = NotificadorMock()
servicio = ServicioPedidos(repositorio, notificador)
pedido = {"estado": "pendiente"}
servicio.aprobar(pedido)
assert pedido["estado"] == "aprobado"
assert repositorio.pedidos == [pedido]
assert notificador.mensajes == ["Pedido aprobado"]
La prueba controla las dependencias y evita base de datos o correo reales.
A veces queremos que producción use una dependencia real por defecto, pero que la prueba pueda reemplazarla.
def convertir_a_dolares(monto, servicio_cotizacion=None):
if servicio_cotizacion is None:
servicio_cotizacion = ServicioCotizacionReal()
cotizacion = servicio_cotizacion.obtener_cotizacion()
return monto / cotizacion
Esto permite llamar la función sin pasar dependencia en producción, pero pasar un stub en pruebas. Hay que usar este patrón con cuidado para no ocultar demasiadas decisiones.
No siempre necesitamos objetos. A veces podemos inyectar una función.
def cupon_vigente(fecha_vencimiento, obtener_fecha_actual):
return obtener_fecha_actual() <= fecha_vencimiento
En la prueba pasamos una función que devuelve una fecha fija:
from datetime import date
def test_cupon_vigente():
def fecha_fija():
return date(2026, 6, 1)
assert cupon_vigente(date(2026, 12, 31), fecha_fija) == True
Esto evita depender del reloj real.
A veces no hace falta inyectar un servicio completo. Puede bastar con pasar el dato que la unidad necesita.
def cupon_vigente(fecha_actual, fecha_vencimiento):
return fecha_actual <= fecha_vencimiento
Esta versión es aún más simple. Si la unidad solo necesita la fecha, no hace falta pasar un objeto reloj.
La inyección no debe complicar el código innecesariamente. La pregunta es qué dependencia necesitamos controlar.
| Necesidad | Opción simple |
|---|---|
| Controlar una fecha | Pasar la fecha como dato. |
| Controlar una cotización | Pasar cotización o servicio de cotización. |
| Reemplazar repositorio | Pasar repositorio por constructor. |
| Verificar notificación | Pasar notificador reemplazable. |
La inyección de dependencias aporta varios beneficios:
Además de facilitar pruebas, la inyección de dependencias puede mejorar el diseño. Una unidad que recibe sus dependencias suele ser más explícita y flexible.
En lugar de ocultar que necesita un repositorio, un servicio o un reloj, lo declara en sus parámetros o constructor.
Esto ayuda a separar responsabilidades: una clase no tiene que saber cómo construir todas sus dependencias para poder usarlas.
No todo debe inyectarse. Si inyectamos demasiadas cosas, el código puede volverse difícil de usar y entender.
Señales de exceso:
La inyección debe resolver un problema real de acoplamiento, control o testeabilidad.
Una dependencia global puede dificultar las pruebas porque la unidad la usa sin que aparezca en su firma.
configuracion_global = {"moneda": "ARS"}
def mostrar_precio(precio):
return f"{configuracion_global['moneda']} {precio}"
La prueba depende de una variable global. Una alternativa es pasar la moneda o configuración explícitamente.
def mostrar_precio(precio, moneda):
return f"{moneda} {precio}"
def test_mostrar_precio_en_pesos():
assert mostrar_precio(100, "ARS") == "ARS 100"
La unidad es más simple y la prueba no depende de configuración global mutable.
| Forma | Cuándo usarla | Ejemplo |
|---|---|---|
| Parámetro | Funciones simples. | Pasar servicio de cotización. |
| Constructor | Clases con dependencias. | ServicioPedidos recibe repositorio. |
| Función | Comportamiento reemplazable simple. | Función que devuelve fecha actual. |
| Dato directo | Cuando no hace falta un objeto completo. | Pasar moneda o fecha. |
| Dependencia por defecto | Cuando producción necesita valor real por defecto. | Servicio real si no se pasa stub. |
Al aplicar inyección de dependencias, revisa:
La inyección de dependencias es una técnica sencilla que facilita mucho las pruebas unitarias. Al recibir dependencias desde afuera, una unidad puede trabajar con implementaciones reales en producción y con dobles controlados en pruebas.
El objetivo no es agregar complejidad, sino reducir acoplamiento y hacer que el comportamiento sea más fácil de verificar.
En el próximo tema veremos diseño de código testeable, una visión más general de cómo escribir unidades fáciles de probar.