30. Inyección de dependencias para facilitar pruebas

30.1 Introducción

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.

30.2 Qué es una dependencia

Una dependencia es algo que una unidad necesita para realizar su trabajo. Puede ser un objeto, función, servicio, configuración o repositorio.

Ejemplos:

  • Un servicio que obtiene cotizaciones.
  • Un repositorio que guarda usuarios.
  • Un notificador que envía correos.
  • Un reloj que devuelve la fecha actual.
  • Una configuración que indica la moneda.

Si la unidad crea esas dependencias por su cuenta, probarla de forma aislada puede volverse difícil.

30.3 Problema: crear la dependencia internamente

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.

30.4 Solución: recibir la dependencia

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.

30.5 Prueba con 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.

30.6 Inyección por parámetro

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"

30.7 Inyección por constructor

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.

30.8 Prueba con dependencias inyectadas

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.

30.9 Dependencias por defecto

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.

30.10 Inyección de funciones

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.

30.11 Inyección de datos simples

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.

30.12 Elegir el nivel adecuado

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.

30.13 Beneficios para pruebas

La inyección de dependencias aporta varios beneficios:

  • Permite usar stubs, mocks y fakes.
  • Evita dependencias externas reales en pruebas unitarias.
  • Mejora la repetibilidad.
  • Reduce el acoplamiento.
  • Hace más visible qué necesita la unidad.
  • Facilita probar casos difíciles de provocar con dependencias reales.

30.14 Beneficios para diseño

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.

30.15 Riesgo: exceso de inyección

No todo debe inyectarse. Si inyectamos demasiadas cosas, el código puede volverse difícil de usar y entender.

Señales de exceso:

  • Funciones con demasiados parámetros.
  • Clases que reciben muchas dependencias.
  • Inyección de valores triviales que no necesitan reemplazo.
  • Diseño más complejo que el problema original.

La inyección debe resolver un problema real de acoplamiento, control o testeabilidad.

30.16 Riesgo: ocultar dependencias globales

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.

30.17 Versión con dependencia explícita

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.

30.18 Tabla de patrones

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.

30.19 Lista de comprobación

Al aplicar inyección de dependencias, revisa:

  • ¿Qué dependencia dificulta la prueba?
  • ¿Realmente necesita reemplazarse?
  • ¿Puede pasarse como dato simple?
  • ¿Conviene pasar una función, objeto o servicio?
  • ¿La unidad queda más clara o más compleja?
  • ¿La prueba puede usar un doble simple?
  • ¿La dependencia real se probará en otro nivel si corresponde?

30.20 Qué debes recordar de este tema

  • Inyectar una dependencia significa recibirla desde afuera.
  • Esto facilita reemplazar dependencias reales por dobles de prueba.
  • Puede hacerse por parámetro, constructor, función o dato directo.
  • Evita crear servicios reales dentro de la unidad probada.
  • Ayuda a aislar lógica de dependencias externas.
  • No todo debe inyectarse; debe haber una razón práctica.
  • La inyección mejora la testeabilidad cuando reduce acoplamiento.

30.21 Conclusión

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.