Módulo 7: CI Integration con GitHub Actions

Correr pytest en GitHub Actions

Correr pytest en GitHub Actions

Descripción de la cápsula

En la cápsula anterior configuraste un workflow básico: checkout, setup Python, install deps, run pytest. Pero en proyectos reales necesitas más: ver claramente qué tests fallaron, guardar reportes como artifacts, ejecutar solo unit tests o solo integration tests según el contexto, y manejar variables de entorno como API keys en un entorno seguro.

Esta cápsula profundiza en ejecutar pytest en CI: desglose de los steps, manejo de salida y fallos, publicación de resultados como artifacts, ejecución de categorías específicas de tests (unit, integration, e2e), interpretación de logs en la UI de GitHub, problemas frecuentes y uso de secrets para variables sensibles.

Al final tendrás un pipeline robusto que te da visibilidad clara cuando algo falla y que maneja correctamente las dependencias y configuraciones de tu proyecto.


Desglose de los steps para pytest en CI

Secuencia típica

Step 1: Checkout      → Obtener el código
Step 2: Setup Python → Versión correcta
Step 3: Install deps → pytest, pytest-cov, dependencias del proyecto
Step 4: Run pytest   → Ejecutar tests
Step 5 (opcional): Upload artifacts → Guardar reportes

Step 1: Checkout

- name: Checkout repository
  uses: actions/checkout@v4

Sin checkout, el runner está vacío. Esta acción clona el repo en el directorio de trabajo. Por defecto usa la ref del evento (el commit del push o el head del PR).

Step 2: Setup Python

- name: Set up Python
  uses: actions/setup-python@v5
  with:
    python-version: "3.12"

Indica la versión que usas en desarrollo. Si tu proyecto soporta varias versiones, usarás matrix en la cápsula 04.

Step 3: Install dependencies

- name: Install dependencies
  run: |
    python -m pip install --upgrade pip
    pip install -r requirements.txt

Si tienes dependencias de test separadas:

# requirements.txt
fastapi>=0.100.0
uvicorn

# requirements-dev.txt (o en pyproject.toml [project.optional-dependencies] dev)
pytest
pytest-cov
httpx

Entonces:

- name: Install dependencies
  run: |
    python -m pip install --upgrade pip
    pip install -r requirements.txt
    pip install -r requirements-dev.txt

Step 4: Run pytest

- name: Run tests
  run: pytest tests/ -v

Por defecto pytest busca tests en test_*.py o *_test.py. El path tests/ indica el directorio. Si tu estructura es diferente (ej. test/ o tests en la raíz), ajusta el path.

Flags útiles para CI:

FlagUso
-vVerbose: muestra cada test
--tb=shortTracebacks cortos en fallos
--no-headerMenos ruido en logs
-xDetener en el primer fallo (opcional)
-qModo quiet (menos verbose)

Ejemplo con flags recomendados para CI:

- name: Run tests
  run: pytest tests/ -v --tb=short --no-header -q

Manejo de salida y fallos

Qué pasa cuando un test falla

pytest retorna exit code 1 cuando algún test falla. El runner de GitHub Actions interpreta eso como fallo del step. El job falla y el workflow marca la ejecución como failed.

Ver el fallo en los logs

En la UI de GitHub, abre el step "Run tests" que falló. Verás algo como:

============================= test session starts ==============================
tests/test_auth.py::test_login_success PASSED                            [ 25%]
tests/test_auth.py::test_login_invalid_credentials FAILED                  [ 50%]
tests/test_items.py::test_list_items PASSED                                [ 75%]
tests/test_items.py::test_create_item PASSED                               [100%]

=================================== FAILURES ===================================
_____________ test_login_invalid_credentials _____________
    def test_login_invalid_credentials():
        response = client.post("/login", json={"user": "x", "pass": "y"})
>       assert response.status_code == 401
E       AssertionError: assert 200 == 401
E        +  where 200 = <Response [200 OK]>.status_code

tests/test_auth.py:42: AssertionError
======================== 1 failed, 3 passed in 2.15s =========================

La línea clave es > assert response.status_code == 401. Ahí pytest te muestra el assert que falló y el valor real vs esperado.

Usar -x para fallar rápido

Si tienes 500 tests y el primero que falla rompe una cadena de dependencias, a veces conviene detener en el primer fallo:

- name: Run tests
  run: pytest tests/ -v -x

Con -x, pytest se detiene en el primer FAILED y no ejecuta el resto. Ahorra tiempo cuando el primer fallo ya indica un problema grave. Para CI de PRs, muchos prefieren correr todos los tests para ver el panorama completo; -x es útil en desarrollo local o en pipelines muy largos.

Exit code y comportamiento del job

Si pytest retorna 1, el step falla. Los steps siguientes del mismo job no se ejecutan. Si tienes un step "Upload coverage" después de "Run tests", ese upload no correrá si los tests fallaron. Por eso a veces se usa continue-on-error para uploads que quieres ejecutar incluso con fallos (para ver coverage parcial), pero en general quieres que el job falle cuando los tests fallan.


Publicar resultados como artifacts

¿Qué es un artifact?

Un artifact es un archivo o directorio que el workflow guarda después de que el job termina. Puedes descargarlo desde la UI de GitHub. Útil para reportes HTML de coverage, resultados de pytest en formato JUnit XML, o logs.

Subir htmlcov como artifact

- name: Run tests with coverage
  run: |
    pip install pytest-cov
    pytest tests/ -v --cov=src --cov-report=html

- name: Upload coverage report
  uses: actions/upload-artifact@v4
  if: always()  # Sube incluso si tests fallan
  with:
    name: coverage-report
    path: htmlcov/

Después de la ejecución, en la página del workflow run verás "Artifacts" con un enlace para descargar coverage-report.zip. Dentro está el contenido de htmlcov/ — descomprimes y abres index.html para ver el reporte.

Subir resultados JUnit XML

pytest puede generar resultados en formato JUnit XML, que GitHub Actions puede parsear para mostrar un resumen de tests en la UI.

- name: Run tests
  run: pytest tests/ -v --junitxml=test-results.xml

- name: Publish test results
  uses: EnricoMi/publish-unit-test-result-action@v2
  if: always()
  with:
    files: test-results.xml

Esta acción muestra en la UI una tabla con tests pasados/fallados. Requiere instalar la acción; hay alternativas como dorny/test-reporter o simplemente subir el XML como artifact y descargarlo para análisis local.

Artifact solo si hay fallos

- name: Upload logs on failure
  uses: actions/upload-artifact@v4
  if: failure()
  with:
    name: pytest-output
    path: test-results.xml

Solo sube el artifact cuando el job falla. Útil para no acumular artifacts innecesarios en ejecuciones exitosas.


Ejecutar categorías de tests

Marcando tests con markers

En pytest.ini o pyproject.toml:

[tool.pytest.ini_options]
markers = [
    "unit: Unit tests (rápidos, sin I/O)",
    "integration: Integration tests (DB, API, red)",
    "e2e: End-to-end tests (lentos)",
]

En los tests:

import pytest

@pytest.mark.unit
def test_add():
    assert add(2, 3) == 5

@pytest.mark.integration
def test_api_create_item():
    response = client.post("/items", json={"name": "x"})
    assert response.status_code == 201

@pytest.mark.e2e
def test_full_user_flow():
    # Test que abre browser, navega, etc.
    pass

Correr solo unit en CI rápido

- name: Run unit tests
  run: pytest tests/ -v -m unit

Solo ejecuta tests marcados con @pytest.mark.unit. Útil si quieres un job rápido que corre en cada push, y un job más lento con integration/e2e que corre solo en PRs a main.

Correr unit + integration en CI estándar

- name: Run tests
  run: pytest tests/ -v -m "unit or integration"

O excluir e2e:

- name: Run tests (excluyendo e2e)
  run: pytest tests/ -v -m "not e2e"

Jobs separados para diferentes categorías

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v -m unit

  integration:
    runs-on: ubuntu-latest
    needs: unit  # Solo corre si unit pasa
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v -m integration

El job integration solo corre si unit pasa. Ahorra tiempo cuando unit ya falló.


Interpretar logs de fallos en la UI de GitHub

Estructura típica del log

  1. Salida de pip: Si el step de install falla, verás el error de pip (paquete no encontrado, conflicto de versiones, etc.).
  2. Salida de pytest: La sesión de tests, cada test con PASSED/FAILED, y al final el resumen.
  3. Traceback: Cuando un test falla, pytest imprime el traceback. La línea con > indica el assert que falló.

Errores comunes en los logs

MensajeCausa probableAcción
ModuleNotFoundError: No module named 'X'Falta instalar X o el path no incluye el paqueteAñadir X a requirements, o configurar pythonpath
ImportError: cannot import name 'Y'Y no existe o está en otro móduloRevisar imports en el código
Fixture 'Z' not foundFixture en conftest que no se cargó o typoVerificar conftest.py, scope, nombres
FAILED tests/test_x.py::test_yEl test fallóIr al traceback, ver assert y valor real
No module named 'tests'El directorio tests no está en el pathEjecutar desde raíz: pytest tests/
Collecting ... 0 itemspytest no encontró testsVerificar que los archivos empiecen con test_ y las funciones con test_

Flujo de diagnóstico cuando falla el job

  1. Abre el workflow run en la pestaña Actions.
  2. Identifica el job que falló (marca roja).
  3. Clic en el job y busca el step que falló (el primero con X roja).
  4. Si es "Install dependencies": revisa el error de pip. Suele ser paquete inexistente o conflicto de versiones.
  5. Si es "Run tests": lee el traceback. La línea con > indica el assert. El valor mostrado (E AssertionError: assert X == Y) te dice qué esperabas vs qué obtuviste.
  6. Copia el fragmento relevante (desde ===== FAILURES ===== hasta el final del traceback) y úsalo con Claude Code o en una búsqueda para diagnosticar.

Buscar en los logs

En la página del step, usa Ctrl+F (Cmd+F) para buscar:

  • FAILED — Encuentra rápidamente qué tests fallaron.
  • Error o error: — Errores de import o configuración.
  • ModuleNotFoundError — Para diagnosticar problemas de dependencias.

Variables de entorno en CI

Variables de entorno básicas

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: "sqlite:///./test.db"
      LOG_LEVEL: "WARNING"
      PYTHONUNBUFFERED: "1"
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: pytest tests/

Los tests pueden leer os.environ["DATABASE_URL"] o usar os.getenv("DATABASE_URL"). Útil para tests de integración que usan una DB de prueba.

Usar GitHub Secrets para valores sensibles

Nunca pongas API keys, tokens o passwords en el YAML. Usa Secrets:

  1. En GitHub: Settings → Secrets and variables → Actions.
  2. New repository secret. Nombre: API_KEY, valor: tu clave.
  3. En el workflow:
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      API_KEY: ${{ secrets.API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/

Si el secret no existe, secrets.API_KEY será vacío. Asegúrate de crear el secret antes de que corra el workflow.

Secrets para tests que llaman APIs reales

Algunos proyectos tienen tests que llaman a APIs externas (sandbox) y necesitan un token. Configura el secret en el repo y pásalo como env. En los tests, si API_KEY está vacío, puedes saltarte el test:

import os
import pytest

@pytest.mark.skipif(not os.getenv("API_KEY"), reason="API_KEY not set")
def test_external_api():
    # Test que usa la API real con el token
    pass

Variables de entorno por step

steps:
  - name: Run unit tests
    run: pytest tests/ -m unit
    env:
      USE_MOCK: "1"

  - name: Run integration tests
    run: pytest tests/ -m integration
    env:
      DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

Cada step puede tener su propio bloque env que sobrescribe o amplía el del job.


Práctica guiada: pipeline con artifacts y markers

Sigue estos pasos para construir un pipeline completo con pytest, coverage y artifacts.

1. Crear estructura de tests con markers

# tests/test_calc.py
import pytest
from src.calc import add, multiply

@pytest.mark.unit
def test_add():
    assert add(2, 3) == 5

@pytest.mark.unit
def test_multiply():
    assert multiply(3, 4) == 12

2. Registrar markers en pyproject.toml

[tool.pytest.ini_options]
markers = ["unit: Unit tests"]
testpaths = ["tests"]

3. Añadir coverage y artifacts al workflow

Modifica el step de pytest para generar coverage y subir el reporte:

- name: Run tests with coverage
  run: |
    pip install pytest-cov
    pytest tests/ -v -m unit --cov=src --cov-report=html --cov-report=term-missing

- name: Upload coverage report
  uses: actions/upload-artifact@v4
  if: always()
  with:
    name: coverage-report
    path: htmlcov/

4. Verificar en GitHub

Haz push, espera a que el workflow termine, y en la página de la ejecución descarga el artifact coverage-report. Descomprime y abre htmlcov/index.html para ver el reporte visual de coverage.

5. Probar con un test que falla

Intencionalmente cambia un assert para que falle. Haz push. Verás que el workflow marca failed, pero el artifact de coverage se subió igual (por if: always()). Los logs del step "Run tests" mostrarán el traceback del test fallido.


Interpretación detallada: de la UI al fix

Cuando un workflow falla, el flujo de diagnóstico es:

  1. Actions → Run fallida → Clic en la ejecución.
  2. Job fallido → Clic en el job (ej. "test") para ver los steps.
  3. Step fallido → Clic en el step con X roja. Suele ser "Run tests" o "Install dependencies".
  4. Log del step → Scroll hasta el final. El error suele estar ahí. Busca FAILED, Error, Traceback.
  5. Copia el traceback → Desde def test_xxx hasta la línea del assert. Ese bloque es lo que necesitas para corregir.

Si el fallo es en "Install dependencies", el problema está en requirements o en la sintaxis del comando pip. Si es en "Run tests", el problema está en el código o en los tests. El traceback te dice el archivo y la línea exacta.

Resumen de flags pytest para CI

FlagPropósito
-vVerbose: nombre de cada test
--tb=shortTraceback corto en fallos (menos líneas)
--tb=lineSolo la línea del error
-xDetener en el primer fallo
-qQuiet: menos output
--no-headerSin banner de sesión
-m unitSolo tests con marker unit
-m "not slow"Excluir tests marcados slow

Combinación recomendada para CI: pytest tests/ -v --tb=short — suficiente información sin saturar los logs.

Ejemplo: diagnosticar un ModuleNotFoundError

Si el log muestra:

ModuleNotFoundError: No module named 'src'

Pasos:

  1. Verifica que el código esté en src/ y que los tests importen from src.X import Y.
  2. El runner ejecuta desde la raíz del repo (tras checkout). Si src no está en PYTHONPATH, Python no lo encuentra.
  3. Añade env: PYTHONPATH: . al job, o instala el paquete con pip install -e . si tienes pyproject.toml.
  4. Haz push y verifica. Si el error persiste, puede que la estructura de directorios sea distinta (ej. app/ en lugar de src/); ajusta el path.

Cuándo usar markers vs jobs separados

EstrategiaUso
Un job con markersProyectos pequeños, todos los tests corren en cada push. pytest -m unit o pytest -m "not e2e".
Jobs separadosUnit rápido siempre; integration solo en PR o en schedule. needs: unit para que integration espere.
Job de e2e en scheduleE2E lentos (browser, APIs externas) en cron: on: schedule: - cron: '0 2 * * *' (2am diario).

Para el proyecto del Módulo 7, un solo job con todos los tests suele ser suficiente. Si tu suite tarda más de 5 minutos, considera separar unit (rápido, cada push) de integration (más lento, cada PR).


Workflow completo con pytest en CI

# .github/workflows/tests.yml
name: Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    env:
      PYTHONUNBUFFERED: "1"

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install pytest pytest-cov

      - name: Run tests
        run: pytest tests/ -v --tb=short --cov=src --cov-report=html --cov-report=term-missing

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: htmlcov/

Este workflow instala deps, corre pytest con coverage, y sube el reporte HTML como artifact. El artifact se genera tanto si los tests pasan como si fallan (if: always()), así puedes inspeccionar el coverage incluso cuando hay fallos.


Ejercicios

Ejercicio 1: Añadir flags de pytest (Básico)

Modifica el step de pytest de tu workflow para usar -v --tb=short. Haz que un test falle intencionalmente, pushea, y verifica que el log muestre un traceback legible con el assert que falló.

Ver solución
- name: Run tests
  run: pytest tests/ -v --tb=short

En el test, pon assert False o assert 1 == 2. Pushea. En Actions, abre el step "Run tests". Deberías ver algo como:

>       assert False
E       AssertionError

El flag --tb=short hace el traceback más compacto. Sin él, pytest muestra el traceback completo con contexto de cada frame.

Ejercicio 2: Publicar htmlcov como artifact (Intermedio)

Añade pytest-cov a tu proyecto. Configura el workflow para generar htmlcov/ y subirlo como artifact. Ejecuta el workflow, descarga el artifact, descomprímelo y abre htmlcov/index.html en el navegador.

Ver solución
- name: Install dependencies
  run: |
    pip install -r requirements.txt
    pip install pytest-cov

- name: Run tests with coverage
  run: pytest tests/ --cov=src --cov-report=html

- name: Upload coverage report
  uses: actions/upload-artifact@v4
  with:
    name: coverage-report
    path: htmlcov/

Asegúrate de que --cov=src coincida con la estructura de tu proyecto (puede ser app, my_package, etc.). Después de la ejecución, en la página del workflow run verás "Artifacts" → Descargar "coverage-report". Descomprime y abre index.html.

Ejercicio 3: Ejecutar solo unit tests (Intermedio)

Tu proyecto tiene tests marcados con @pytest.mark.unit y @pytest.mark.integration. Configura el workflow para correr solo los unit tests. Añade un marker unit en pyproject.toml si no lo tienes.

Ver solución

En pyproject.toml:

[tool.pytest.ini_options]
markers = ["unit: Unit tests"]

En el workflow:

- name: Run unit tests
  run: pytest tests/ -v -m unit

Si no defines el marker, pytest puede advertir. Definirlo en markers evita la advertencia y documenta el propósito.

Ejercicio 4: Variable de entorno para test (Intermedio)

Tienes un test que usa os.getenv("TEST_MODE") para decidir si usar mocks o una DB real. Añade TEST_MODE=1 como variable de entorno en el job de tests del workflow.

Ver solución
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      TEST_MODE: "1"
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: pytest tests/

En el test: mode = os.getenv("TEST_MODE", "0") — en CI será "1", localmente "0" por defecto.

Ejercicio 5: Diagnosticar ModuleNotFoundError (Avanzado)

El workflow falla con ModuleNotFoundError: No module named 'src'. Tu proyecto tiene src/my_module/ y los tests hacen from src.my_module import something. ¿Qué puedes cambiar en el workflow o en la estructura para que funcione?

Ver solución

Opciones:

  1. Añadir el directorio raíz al PYTHONPATH:
env:
  PYTHONPATH: .

O en el step de pytest:

- name: Run tests
  run: pytest tests/
  env:
    PYTHONPATH: .
  1. Instalar el paquete en modo editable: Si tienes pyproject.toml con el paquete configurado:
- run: pip install -e .

Eso añade el paquete al path.

  1. Ejecutar pytest con el path: Algunos proyectos usan python -m pytest tests/ que puede resolver mejor los imports según la estructura.

Ejercicio 6: Secret para API key (Avanzado)

Tienes un test de integración que llama a una API externa y necesita API_KEY. Configura un secret en el repo (aunque sea un valor fake para la práctica) y úsalo en el workflow. Verifica que el test lo reciba.

Ver solución
  1. Settings → Secrets and variables → Actions → New repository secret.
  2. Nombre: API_KEY, Valor: (tu clave o un valor fake tipo test-key-123).

En el workflow:

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      API_KEY: ${{ secrets.API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: pytest tests/

En el test puedes hacer key = os.getenv("API_KEY") y usarla para la llamada. Si el secret no existe, será cadena vacía. Puedes añadir @pytest.mark.skipif(not os.getenv("API_KEY"), reason="No API_KEY") para saltar el test cuando no hay key.


Troubleshooting

Tests pasan local pero fallan en CI

Causa: Diferencias de entorno: versión de Python, dependencias, variables de entorno, timezone, paths.

Solución: Revisa que la versión de Python en el workflow coincida con la local. Si usas pathlib o rutas relativas, verifica que el directorio de trabajo sea el esperado (tras checkout es la raíz del repo). Ejecuta localmente en un venv limpio: pip install -r requirements.txt && pytest tests/ para simular CI.

"Fixture 'X' not found"

Causa: La fixture está definida en un conftest.py que pytest no está descubriendo, o hay un typo en el nombre.

Solución: Asegúrate de que conftest.py esté en tests/ o en la raíz. pytest lo descubre automáticamente. Si la fixture está en un conftest de un subdirectorio, verifica que los tests que la usan estén en ese subdirectorio o en hijos. Comprueba que el nombre de la fixture coincida exactamente.

Dependencias que faltan en CI

Causa: Algún paquete está instalado globalmente o en tu venv local pero no está en requirements.txt (o en pyproject.toml).

Solución: Haz pip freeze en tu venv local y compara con lo que instala el workflow. Todo lo que importas en tests debe estar en requirements. Si usas pytest-cov, httpx, etc., añádelos a requirements-dev o a la sección de test de pyproject.toml.

Import errors con estructura src/

Causa: El paquete src o app no está en el PYTHONPATH cuando pytest corre.

Solución: Añade PYTHONPATH: . al env del job, o instala el paquete con pip install -e .. En proyectos con src/ layout, pip install -e . suele configurar correctamente el path.

Artifact no se genera

Causa: El step que sube el artifact falló antes, o el path no existe, o if impide que corra.

Solución: Verifica que el step anterior genere el directorio/archivo (por ejemplo htmlcov/). Si usas if: success() (implícito sin if), el upload solo corre si el job pasa; si los tests fallan, no sube. Usa if: always() para subir siempre. Revisa que el path sea correcto (ej. htmlcov/ con la barra si es directorio).


Conexión con Proyecto

El pipeline de pytest en CI que construiste aquí es la base del proyecto del Módulo 7. En la cápsula 04 añadirás matrix testing (múltiples versiones de Python) y caching para acelerar builds. En la 05 integrarás coverage en los PRs y branch protection. Los steps que definiste — install, run pytest, upload artifacts — seguirán siendo los bloques centrales.


Resumen

  • Los steps típicos para pytest en CI: checkout, setup Python, install deps, run pytest
  • Cuando un test falla, pytest retorna 1 y el job falla; los logs muestran el traceback
  • Puedes publicar reportes (htmlcov, JUnit XML) como artifacts para descargar
  • Usa markers para ejecutar solo unit, integration o e2e según el job
  • Las variables de entorno se configuran en env: del job o del step
  • Los secrets se usan como ${{ secrets.NOMBRE }} para valores sensibles
  • Problemas comunes: ModuleNotFoundError, fixture not found, path/PYTHONPATH

Próxima cápsula: Matrix Testing y Caching — múltiples versiones de Python y builds más rápidos.


Recursos Adicionales

  1. pytest command line options - Flags de pytest
  2. GitHub Actions: upload-artifact - Subir artifacts
  3. pytest markers - Documentación de markers
  4. GitHub Encrypted secrets - Uso de secrets
  5. pytest JUnit XML - Formato JUnit para resultados

Módulo 7, Cápsula 03 — Testing with Claude Code Guide