# Testing Python — Ejemplos completos y referencia

## Mocking con unittest.mock y pytest-mock

```python
from unittest.mock import MagicMock, AsyncMock, patch
import pytest

# Mock de una función de módulo
def test_crear_factura_llama_servicio_timbre(factura_service, mock_timbre):
    with patch("app.services.factura_service.timbre_service") as mock_timbre:
        mock_timbre.timbrar = MagicMock(return_value={"uuid": "abc-123"})

        factura = factura_service.crear(datos_validos)

        mock_timbre.timbrar.assert_called_once_with(factura)
        assert factura.uuid_timbre == "abc-123"

# Con pytest-mock (más limpio)
def test_notificacion_enviada(mocker, factura_service):
    mock_email = mocker.patch("app.services.email_service.enviar")

    factura_service.emitir(factura_id="fac-123")

    mock_email.assert_called_once()
    llamada = mock_email.call_args
    assert llamada.kwargs["destinatario"] == "cliente@empresa.com"

# Mock de async
async def test_obtener_usuario_llama_api_externa(mocker):
    mock_get = mocker.patch("httpx.AsyncClient.get", new_callable=AsyncMock)
    mock_get.return_value.json.return_value = {"id": "u1", "nombre": "Ana"}
    mock_get.return_value.status_code = 200

    usuario = await servicio.obtener_usuario("u1")

    assert usuario.nombre == "Ana"
    mock_get.assert_awaited_once()
```

---

## Testing async con pytest-asyncio

```python
# pyproject.toml
# [tool.pytest.ini_options]
# asyncio_mode = "auto"  # Marcar todos los tests async automáticamente

import pytest
from httpx import AsyncClient, ASGITransport

@pytest.mark.asyncio
async def test_crear_factura_endpoint(async_client: AsyncClient):
    respuesta = await async_client.post(
        "/api/v1/facturas/",
        json={
            "folio": "F-001",
            "fecha": "2026-03-25",
            "subtotal": "1000.00",
            "cliente_id": "cliente-123",
        },
        headers={"Authorization": "Bearer token-test"},
    )
    assert respuesta.status_code == 201
    data = respuesta.json()
    assert data["estatus"] == "borrador"
    assert "id" in data

@pytest.fixture
async def async_client(app, async_db):
    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://test",
    ) as client:
        yield client
```

---

## Factories con factory_boy

```python
# tests/factories.py
import factory
from decimal import Decimal
from datetime import date
from app.models import Factura, Cliente, Item

class ClienteFactory(factory.Factory):
    class Meta:
        model = Cliente

    id = factory.Sequence(lambda n: f"cliente-{n:05d}")
    nombre = factory.Faker("company", locale="es_MX")
    rfc = factory.LazyFunction(lambda: f"RFC{factory.Faker('numerify', text='######')}")
    activo = True

class ItemFactory(factory.Factory):
    class Meta:
        model = Item

    descripcion = factory.Faker("sentence", nb_words=4, locale="es_MX")
    cantidad = factory.Faker("pyint", min_value=1, max_value=10)
    precio_unitario = factory.Faker("pydecimal", min_value=10, max_value=1000,
                                    right_digits=2)

class FacturaFactory(factory.Factory):
    class Meta:
        model = Factura

    id = factory.Sequence(lambda n: f"fac-{n:05d}")
    folio = factory.Sequence(lambda n: f"F-{n:05d}")
    fecha = factory.LazyFunction(date.today)
    estatus = "borrador"
    cliente = factory.SubFactory(ClienteFactory)
    items = factory.List([factory.SubFactory(ItemFactory)])

# Uso en tests
def test_calcular_total():
    items = [ItemFactory(cantidad=2, precio_unitario=Decimal("100.00"))]
    factura = FacturaFactory(items=items)
    assert factura.calcular_total() == Decimal("200.00")
```

---

## Anti-patrones en tests

```python
# MAL: test que verifica demasiado
def test_factura():
    f = Factura(...)
    assert f.folio == "F-001"
    assert f.estatus == "borrador"
    assert f.total == 1160
    assert f.cliente.nombre == "Acme"
    assert f.puede_cancelar() == True
    # ... 10 asserts más

# BIEN: un comportamiento por test
def test_factura_nueva_tiene_estatus_borrador():
    factura = FacturaFactory()
    assert factura.estatus == "borrador"

def test_factura_calcula_total_con_iva():
    factura = FacturaFactory(subtotal=Decimal("1000.00"), tasa_iva=0.16)
    assert factura.total == Decimal("1160.00")

# MAL: lógica de negocio en los tests
def test_calcular_descuento():
    if monto > 1000:      # lógica duplicada del código de producción
        esperado = monto * 0.1
    else:
        esperado = 0
    assert calcular_descuento(monto) == esperado

# BIEN: valores concretos y explícitos
@pytest.mark.parametrize("monto,descuento_esperado", [
    (500,  Decimal("0.00")),
    (1000, Decimal("0.00")),
    (1001, Decimal("100.10")),
    (2000, Decimal("200.00")),
])
def test_calcular_descuento(monto, descuento_esperado):
    assert calcular_descuento(monto) == descuento_esperado
```
