Módulo 14: CI/CD y Testing Automatizado

Objetivos del módulo

  • Comprender los conceptos de CI/CD
  • Configurar GitHub Actions para ejecutar tests automáticamente
  • Integrar pruebas unitarias con dotnet test
  • Añadir pruebas de API y UI al pipeline
  • Publicar reportes de resultados
  • Aplicar buenas prácticas de testing en CI/CD

1. ¿Qué es CI/CD?

graph LR
    A[Commit] --> B[Build]
    B --> C[Unit Tests]
    C --> D[Integration Tests]
    D --> E[Deploy Staging]
    E --> F[UI/API Tests]
    F --> G[Deploy Producción]
    style A fill:#2196F3,color:white
    style C fill:#4CAF50,color:white
    style F fill:#FF9800,color:white
    style G fill:#9C27B0,color:white
Concepto Descripción
CI (Continuous Integration) Compilar y testear automáticamente cada commit
CD (Continuous Delivery) Desplegar automáticamente a staging tras pasar tests
CD (Continuous Deployment) Desplegar automáticamente a producción

¿Por qué CI/CD en Testing?

Sin CI/CD Con CI/CD
Tests manuales, se olvidan Tests automáticos en cada commit
Errores detectados tarde Errores detectados en minutos
“Funciona en mi máquina” Entorno consistente
Despliegues arriesgados Despliegues con confianza

2. GitHub Actions: Conceptos

Estructura

# .github/workflows/nombre.yml
name: CI

on:           # ¿Cuándo se ejecuta?
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:         # Trabajos a ejecutar
  build:
    runs-on: ubuntu-latest    # Máquina virtual
    steps:                     # Pasos secuenciales
      - name: Checkout
        uses: actions/checkout@v4
      - name: Build
        run: dotnet build

Terminología

Término Descripción
Workflow Archivo YAML que define el pipeline
Trigger (on) Evento que inicia el workflow
Job Conjunto de pasos que corren en la misma VM
Step Acción individual dentro de un job
Action Paso reutilizable (marketplace)
Runner Máquina virtual que ejecuta el job

3. Pipeline básico: Build + Unit Tests

name: CI - Build y Tests

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      # 1. Descargar código
      - name: Checkout código
        uses: actions/checkout@v4

      # 2. Instalar .NET 9
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      # 3. Restaurar dependencias
      - name: Restaurar paquetes NuGet
        run: dotnet restore

      # 4. Compilar
      - name: Build
        run: dotnet build --no-restore --configuration Release

      # 5. Ejecutar tests unitarios
      - name: Ejecutar tests
        run: dotnet test --no-build --configuration Release --verbosity normal

4. Tests con cobertura de código

jobs:
  test-with-coverage:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Restaurar y Build
        run: |
          dotnet restore
          dotnet build --no-restore

      # Ejecutar tests con cobertura
      - name: Tests con cobertura
        run: |
          dotnet test --no-build \
            --collect:"XPlat Code Coverage" \
            --results-directory ./coverage

      # Generar reporte HTML
      - name: Instalar ReportGenerator
        run: dotnet tool install -g dotnet-reportgenerator-globaltool

      - name: Generar reporte de cobertura
        run: |
          reportgenerator \
            -reports:"coverage/**/coverage.cobertura.xml" \
            -targetdir:"coverage/report" \
            -reporttypes:"Html;Cobertura"

      # Subir reporte como artefacto
      - name: Subir reporte de cobertura
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/report/

Requisito mínimo de cobertura

      - name: Verificar cobertura mínima
        run: |
          COVERAGE=$(grep -oP 'line-rate="\K[^"]+' coverage/**/coverage.cobertura.xml | head -1)
          PERCENTAGE=$(echo "$COVERAGE * 100" | bc)
          echo "Cobertura: $PERCENTAGE%"
          if (( $(echo "$PERCENTAGE < 80" | bc -l) )); then
            echo "❌ Cobertura inferior al 80%"
            exit 1
          fi
          echo "✅ Cobertura suficiente"

5. Pipeline multi-stage

name: CI/CD Completo

on:
  push:
    branches: [main]

jobs:
  # === STAGE 1: Build y Unit Tests ===
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - run: dotnet restore
      - run: dotnet build --no-restore
      - run: dotnet test --no-build --filter "Category=Unit"

  # === STAGE 2: Tests de integración ===
  integration-tests:
    runs-on: ubuntu-latest
    needs: unit-tests        # Espera a que pasen los unit tests

    services:
      sqlserver:
        image: mcr.microsoft.com/mssql/server:2022-latest
        env:
          SA_PASSWORD: YourStrong!Password123
          ACCEPT_EULA: Y
        ports:
          - 1433:1433

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - run: dotnet restore
      - run: dotnet build --no-restore

      - name: Tests de integración
        run: dotnet test --no-build --filter "Category=Integration"
        env:
          ConnectionStrings__Default: "Server=localhost;Database=TestDb;User=sa;Password=YourStrong!Password123;TrustServerCertificate=True"

  # === STAGE 3: Tests API ===
  api-tests:
    runs-on: ubuntu-latest
    needs: integration-tests

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Instalar Newman
        run: npm install -g newman newman-reporter-htmlextra

      - name: Ejecutar tests Postman
        run: |
          newman run tests/postman/collection.json \
            -e tests/postman/env-ci.json \
            --reporters cli,htmlextra \
            --reporter-htmlextra-export reports/api-report.html

      - name: Subir reporte API
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: api-test-report
          path: reports/

  # === STAGE 4: Tests UI ===
  ui-tests:
    runs-on: ubuntu-latest
    needs: integration-tests

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - run: dotnet restore
      - run: dotnet build --no-restore

      - name: Instalar navegadores Playwright
        run: pwsh MiApp.PlaywrightTests/bin/Debug/net9.0/playwright.ps1 install --with-deps

      - name: Ejecutar tests UI
        run: dotnet test MiApp.PlaywrightTests --no-build

      - name: Subir traces
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-traces
          path: MiApp.PlaywrightTests/bin/Debug/net9.0/traces/

Diagrama del pipeline

graph TD
    A[Push a main] --> B[Unit Tests]
    B --> C[Integration Tests + SQL Server]
    C --> D[API Tests - Newman]
    C --> E[UI Tests - Playwright]
    D --> F[Deploy]
    E --> F
    style B fill:#4CAF50,color:white
    style C fill:#2196F3,color:white
    style D fill:#FF9800,color:white
    style E fill:#FF9800,color:white
    style F fill:#9C27B0,color:white

6. Matriz de navegadores

  cross-browser-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        browser: [chromium, firefox, webkit]
      fail-fast: false

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - run: dotnet build MiApp.PlaywrightTests

      - name: Instalar navegadores
        run: pwsh MiApp.PlaywrightTests/bin/Debug/net9.0/playwright.ps1 install --with-deps

      - name: Tests en ${{ matrix.browser }}
        run: dotnet test MiApp.PlaywrightTests --no-build
        env:
          BROWSER: ${{ matrix.browser }}

7. Badges de estado

Añade un badge al README para mostrar el estado del pipeline:

![CI](https://github.com/USUARIO/REPO/actions/workflows/ci.yml/badge.svg)

Resultado visual:

Badge Estado
CI Tests pasando
CI Tests fallando

8. Buenas prácticas

Tests en CI/CD

Práctica Descripción
Tests rápidos primero Unit tests antes que UI tests
Fail fast Si fallan unit tests, no ejecutar el resto
Paralelizar API tests y UI tests pueden correr en paralelo
Artefactos Subir reportes y screenshots siempre
if: always() Subir reportes incluso si los tests fallan
Determinísticos Tests no deben depender de datos cambiantes
Secrets Nunca hardcodear contraseñas, usar ${{ secrets.XXX }}
Cache Cachear NuGet y npm para acelerar

Cache de NuGet

      - name: Cache NuGet
        uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
          restore-keys: ${{ runner.os }}-nuget-

9. Notificaciones

Slack (ejemplo)

      - name: Notificar a Slack
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: " Los tests han fallado en ${{ github.ref }}"
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

10. Ejercicios

Ejercicio 1

Crea un workflow de GitHub Actions que:

  1. Se ejecute en push a main y en pull requests
  2. Instale .NET 9
  3. Compile el proyecto
  4. Ejecute los tests unitarios
  5. Muestre un badge en el README

Ejercicio 2

Añade al workflow del Ejercicio 1:

  • Cobertura de código con XPlat Code Coverage
  • Generación de reporte HTML con reportgenerator
  • Subida del reporte como artefacto

Ejercicio 3

Crea un pipeline multi-stage con:

  • Stage 1: Unit Tests
  • Stage 2: API Tests (Newman)
  • Stage 3: UI Tests (Playwright, multi-navegador con matrix)
  • Cada stage depende del anterior excepto API y UI que corren en paralelo.

Resumen

Concepto Descripción
CI Compilar y testear automáticamente cada commit
GitHub Actions Plataforma CI/CD integrada en GitHub
Workflow Archivo YAML que define el pipeline
needs Dependencia entre jobs (stages)
matrix Ejecutar en múltiples configuraciones
Artefactos Reportes y screenshots subidos al workflow
Secrets Variables seguras para contraseñas y tokens