Volver al blog

Ingeniería de la confianza: TDD, CI/CD y calidad medible en Laravel 13

30 ago 2026 43 min de lecturaLaravel

Una guía completa y práctica para construir software en el que puedas confiar con Laravel 13: la historia y el porqué de TDD, los indicadores de calidad que sí se miden (deuda técnica, complejidad, cobertura, seguridad), el ciclo rojo-verde-refactor con Pest 4, cómo armar escenarios con migraciones, factories y seeds, mocking y fakes, un entorno local con Docker y Makefile, y un pipeline de GitLab CI/CD que encaja con SonarQube, DevSecOps, las métricas DORA y el trabajo de un SRE. Con sustento académico y basado en un caso real de producción.

Ingeniería de la confianza: TDD, CI/CD y calidad medible en Laravel 13

Un software poco confiable no falla en el código: falla en la factura. Cada bug en producción es una llamada de soporte, un cliente que se va, un despliegue que se revierte a medianoche y un equipo que deja de innovar para apagar incendios. "El sistema falla otra vez", "hay demasiada deuda técnica", "cada release es un salto de fe" son síntomas del mismo mal: falta de confianza. Y la confianza no es suerte ni talento individual —es una propiedad de ingeniería que se construye y se mide.

La buena noticia es que sabemos cómo construirla, y no es teoría nueva. Se apoya en tres prácticas que encajan entre sí: Test Driven Development (escribir la prueba antes que el código), análisis estático y calidad medible (que una máquina evalúe tu código en números, no en opiniones) y automatización de extremo a extremo (que todo eso corra solo en cada cambio). Con Laravel 13 tienes las tres al alcance de la mano, con herramientas de primera clase. Este artículo es el plano completo.

De dónde sale esto

No es teoría de manual. Este recorrido está destilado de una API de producción que mantuve —una plataforma de gestión de flotas— con más de 200 pruebas, análisis estático en cada push y despliegue continuo en GitLab. Traigo ese mismo plano, modernizado al stack de Laravel 13 de 2026: Pest 4, Pint, Larastan, SonarQube y GitLab Runner, y lo conecto con las prácticas que hoy definen a un equipo de alto rendimiento: DevSecOps, métricas DORA y SRE.

Una idea con historia: quién inventó TDD

Conviene saber de dónde viene esto, porque no es una moda. Test Driven Development fue formalizado por Kent Beck a finales de los años 90 como una de las prácticas centrales de Extreme Programming (XP), durante el célebre proyecto C3 (Chrysler Comprehensive Compensation) en 1996. Beck lo cuenta con humildad: dice que no lo inventó, sino que lo redescubrió de un viejo manual de programación que proponía una idea simple y radical —escribe primero la salida que esperas, y luego el código que la produce. En 2002 lo consolidó en su libro Test-Driven Development: By Example, hoy una referencia obligada.

Lo interesante es que la intuición de Beck resultó medible. El estudio industrial de Nagappan, Maximilien, Bhat y Williams (2008) siguió a cuatro equipos —tres en Microsoft y uno en IBM— que adoptaron TDD, y encontró una reducción de la densidad de defectos de entre el 40 % y el 90 % frente a proyectos comparables sin TDD, a cambio de un 15–35 % más de tiempo inicial de desarrollo. La meta-análisis de Rafique y Mišić (2013), que agrupa decenas de estudios, confirma la tendencia: mejor calidad externa, con un coste de productividad que se diluye a medida que el equipo madura. Traducción: inviertes un poco al principio y cobras en confianza durante toda la vida del producto.

Y hay una razón económica todavía más contundente, documentada desde Barry Boehm (Software Engineering Economics, 1981) y confirmada una y otra vez por la industria: el costo de corregir un defecto crece de forma exponencial cuanto más tarde lo encuentras. Un error atrapado al escribir el requisito cuesta una unidad; el mismo error descubierto en producción puede costar cientos o miles de veces más.

Diagrama de barras del costo relativo de corregir un defecto según la fase en que se detecta, en escala logarítmica y con colores de verde a rojo. En requisitos cuesta 1 vez; en diseño de 3 a 6 veces; en código 10 veces; en pruebas de desarrollo de 15 a 40 veces; en pruebas de sistema de 30 a 70 veces; y en producción de 40 a 1000 veces. Una franja verde bajo las primeras fases indica que ahí actúa TDD y es barato; una franja roja bajo las últimas indica que ahí duele y es caro. La conclusión es que conviene mover la detección de errores lo más temprano posible

Esta es, en una sola imagen, la justificación de todo lo que sigue: TDD, el análisis estático y el pipeline empujan la detección hacia la izquierda de esa curva, donde arreglar es barato. No pruebas por disciplina moral; pruebas porque es matemáticamente más barato.

Confiable, pero ¿medible? Los indicadores de calidad

"Código de calidad" suena subjetivo, pero la ingeniería de software lleva décadas poniéndole números. Antes de escribir una sola prueba, hay que entender qué estamos midiendo. Estos son los indicadores que un buen equipo vigila —y que herramientas como SonarQube calculan por ti en cada análisis:

Mantenibilidad

Código duplicado, funciones y archivos demasiado grandes, complejidad ciclomática alta (McCabe, 1976), código spaghetti y alto acoplamiento. Todo lo que hace que un cambio pequeño tome un día entero.

Fiabilidad

Falta de pruebas (cobertura baja), bugs latentes, caminos sin ejercitar. Sin una red de pruebas, cada refactor es una apuesta a ciegas.

Seguridad

Vulnerabilidades conocidas en tu código y en tus dependencias, secretos filtrados en el repositorio, malas prácticas. La calidad que no se ve hasta que alguien la explota.

Convenciones

No adecuación a estándares (PSR-12), falta de comentarios donde importan, nombres inconsistentes. El estilo no es cosmético: reduce la carga mental de todo el equipo.

Aquí viene una forma poderosa de ordenar todo esto. Philippe Kruchten (2012) propuso ver cualquier cosa que produce un equipo según dos ejes: cuánto valor aporta (o cuánto cuesta) y cuán visible es (si lo ve el usuario o vive dentro del código). El resultado es una matriz que aclara por qué la deuda técnica es tan traicionera: es negativa y además invisible.

Matriz de deuda técnica de Kruchten en cuatro cuadrantes, cruzando valor (negativo o positivo) con visibilidad (visible o invisible). Visible y positivo: funcionalidad o feature (nuevo endpoint, pantalla, reporte), lo que TDD ayuda a entregar. Visible y negativo: bug o defecto (errores, respuestas 500, cálculos incorrectos, caídas), lo que cazan las pruebas. Invisible y negativo: deuda técnica (código duplicado y spaghetti, falta de pruebas y comentarios, alto acoplamiento y complejidad, métodos y archivos enormes, vulnerabilidades), lo que mide el análisis estático. Invisible y positivo: arquitectura (capas y límites claros, SOLID e inyección de dependencias, patrones, buena cobertura), lo que construye el refactor. La clave de la confianza está en el nivel invisible: TDD y el análisis estático convierten deuda en arquitectura

Lo que el usuario ve —features y bugs— salta a la vista y nadie lo ignora. Lo invisible es donde se juega la confianza a largo plazo: la deuda técnica (negativa, oculta) te frena en silencio hasta que un día ya no puedes avanzar; la arquitectura (positiva, oculta) es lo que te deja avanzar rápido durante años. TDD y el análisis estático son las herramientas que trabajan el nivel invisible: convierten deuda en arquitectura, comprobación a comprobación.

Los tres pilares de la confianza

Con el mapa de la calidad claro, quédate con las tres prácticas que la construyen. Piénsalas como las columnas de un templo: cada una sostiene una parte, ninguna aguanta sola, y todas se apoyan en el mismo cimiento —un código bien diseñado (SOLID) que se puede probar.

Diagrama de los tres pilares de la confianza representados como un templo. El frontón superior dice "software confiable: desplegar un viernes sin sudar frío" y descansa sobre tres columnas. Pilar 1, TDD (rojo): rojo, verde, refactor, la prueba primero, con Pest 4 y PHPUnit 12, aporta una red de seguridad. Pilar 2, Calidad medible (azul): análisis estático en números, no en opiniones, con Pint, Larastan y SonarQube, mantiene la deuda bajo control. Pilar 3, Automatización (verde): en cada commit, sin depender de la memoria, con pre-commit y GitLab CI/CD, hace que nada se escape. Los tres pilares se apoyan en un cimiento común: SOLID y arquitectura limpia, porque código testeable es código bien diseñado

El primer pilar, TDD, escribe la prueba antes que el código y deja una red de seguridad que crece con el proyecto (Pest 4, con PHPUnit por debajo). El segundo, calidad medible, pone máquinas a leer tu código sin ejecutarlo: Pint unifica el estilo, Larastan caza errores de tipo y lógica, y SonarQube le pone número a la deuda, la cobertura y la seguridad. El tercero, automatización, hace que todo eso corra solo en cada cambio —un hook de pre-commit en tu máquina y un pipeline de GitLab CI/CD que repite las comprobaciones y bloquea lo que no pase. Y debajo de los tres, el cimiento: un diseño limpio que hace el código testeable.

La forma más clara de verlo es seguir un commit desde tu editor hasta producción: cruza siempre las mismas puertas, y en cuanto una se pone en rojo, el viaje se detiene.

Diagrama animado del pipeline de la confianza en Laravel 13: un commit (paquete verde) recorre de izquierda a derecha seis etapas. En tu máquina: escribir código con la prueba primero, y git commit con un hook de pre-commit que corre Pint y php -l. En el GitLab Runner: setup (composer install con cache de vendor), quality (Pint en modo verificación y Larastan), test (Pest con cobertura contra una base de datos MySQL efímera) y deploy. Una barra de cobertura crece hasta la meta del 80% cuando el commit llega a la etapa de test. Si Pint, Larastan o Pest fallan, la puerta se pone en rojo y el pipeline se detiene, bloqueando el merge

Diagrama animado del pipeline de la confianza en Laravel 13, versión vertical para móvil: las seis etapas se apilan de arriba abajo. En tu máquina: Código (test primero) y git commit (pre-commit con Pint y php -l). En el GitLab Runner: setup con composer install y cache de vendor, quality con Pint y Larastan, test con Pest y cobertura contra MySQL efímero, y deploy. Debajo, una barra de cobertura con meta del 80%, y un aviso de que una puerta en rojo detiene el pipeline y bloquea el merge

Vamos por partes, empezando por el suelo sobre el que todo se apoya.

La estructura de un proyecto Laravel confiable

Antes de escribir código conviene saber dónde va cada cosa. Una estructura clara es en sí misma una forma de confianza: cualquiera del equipo encuentra las pruebas, las reglas de negocio y la configuración sin tener que preguntar. Esta es la organización de un proyecto real —una API que separa la aplicación Laravel dentro de src/ y mantiene la infraestructura y las herramientas en la raíz del repositorio:

txt
api/                             # raíz del repositorio
├── docker/                      # imágenes y config (nginx, supervisor, mysql)
├── kubernetes/                  # manifiestos de despliegue
├── docs/                        # documentación del proyecto (MkDocs, ADRs, runbooks)
├── compose.yml                  # servicios: api, mysql, redis, mailpit (Docker Compose v2)
├── Makefile                     # make help · setup · test · qa
├── pre-commit.sh                # hook: php -l + pint + phpstan
├── sonar-project.properties     # configuración de SonarQube
├── README.md                    # cómo arrancar el proyecto en 2 comandos
└── src/                         # la aplicación Laravel
    ├── app/
    │   ├── Http/
    │   │   ├── Controllers/     # controladores finos: reciben y responden
    │   │   └── Requests/        # validación (Form Requests)
    │   ├── Models/              # Eloquent (+ Catalogs, Traits…)
    │   ├── Services/            # lógica de negocio (casos de uso)
    │   ├── Jobs/                # trabajos en cola
    │   ├── Events/  Listeners/  # eventos del dominio y sus reacciones
    │   ├── Policies/            # autorización
    │   └── Rules/               # reglas de validación propias
    ├── config/
    ├── database/
    │   ├── migrations/          # esquema versionado
    │   ├── factories/           # datos de prueba
    │   └── seeders/             # datos base (roles, catálogos)
    ├── routes/
    │   ├── api.php              # entrada de la API
    │   └── api/modules/         # rutas divididas por módulo
    └── tests/
        ├── Unit/                # lógica aislada, sin framework
        ├── Feature/             # casos de uso vía HTTP
        └── Pest.php             # configuración global de Pest

Fíjate en cuatro decisiones que pagan en confianza: la app vive aislada en src/ (la infraestructura —Docker, Kubernetes, CI— no se mezcla con el código); los controladores son finos y delegan en Services/, donde vive la lógica de negocio (fácil de probar en aislamiento); las pruebas están separadas por tipo (Unit vs Feature); y hay una carpeta docs/ para la documentación (decisiones de arquitectura, guías de operación, la API), versionada junto al código para que no se quede atrás. El README.md en la raíz cierra el círculo: en dos comandos, cualquiera arranca.

Sobre esta estructura y los valores por defecto de Laravel 13

Dos matices honestos. Primero, meter la app dentro de src/ es una convención del proyecto para separar código de infraestructura; Laravel, por defecto, coloca app/, routes/, tests/… directamente en la raíz. Segundo, el esqueleto de Laravel 13 es slim: su app/ viene casi vacío (Http/Controllers, Models, Providers) y la configuración central vive en bootstrap/app.php. Carpetas como Services/, Rules/, Policies/, Events/ y Listeners/ las creas tú a medida que el dominio crece —no son magia del framework, son buenas costumbres. Lo importante no es copiar este árbol al pie de la letra, sino la idea: un lugar predecible para cada cosa.

El entorno local reproducible

El primer enemigo de la confianza es el clásico "en mi máquina funciona". Si tu entorno depende de qué versión de PHP tienes instalada, de qué extensiones y de qué base de datos, cada persona corre un proyecto distinto —y el pipeline, otro más. La cura es empaquetar el entorno con Docker para que sea idéntico en tu laptop, en la de tu colega y en el runner de CI.

Diagrama de arquitectura del entorno local reproducible en Laravel 13: el desarrollador ejecuta comandos a través de un Makefile, que actúa como superficie de control y levanta con Docker Compose una red interna de servicios. El servicio api usa la imagen todo-en-uno webdevops/php-nginx 8.3, que trae nginx, PHP-FPM 8.3 y supervisor (colas y scheduler) en un solo contenedor, con el código de Laravel 13 montado en vivo desde la carpeta src. Los demás servicios son mysql 8 con volumen persistente, redis para caché, sesiones y colas, y mailpit para capturar los correos en desarrollo con panel web en el puerto 8025. La api habla con MySQL vía Eloquent, con Redis para caché y colas, y con Mailpit por SMTP

Con Laravel puedes usar Sail (el entorno Docker oficial) o un compose.yml propio si necesitas más control. Este es el enfoque de la API real: una imagen todo-en-uno webdevops/php-nginx (trae PHP-FPM y nginx juntos, con supervisor para las colas), más MySQL, Redis y Mailpit para capturar los correos en desarrollo. La app se monta desde src/:

yaml
# compose.yml   (Docker Compose v2 · se ejecuta con "docker compose")
networks:
  app_net: { driver: bridge }

volumes:
  db_data:
  redis_data:

services:
  api:                              # PHP 8.3 + nginx en una sola imagen
    image: webdevops/php-nginx:8.3-alpine
    container_name: api
    working_dir: /app
    volumes:
      - ./src:/app:delegated        # la app Laravel vive en src/
    environment:
      WEB_DOCUMENT_ROOT: /app/public
    ports: ['80:80']
    networks: [app_net]

  mysql:
    image: mysql:8
    container_name: mysql
    environment:
      MYSQL_DATABASE: '${DB_DATABASE}'
      MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
    volumes: ['db_data:/var/lib/mysql']
    ports: ['${FORWARD_DB_PORT:-3306}:3306']
    networks: [app_net]
    healthcheck:
      test: ['CMD', 'mysqladmin', 'ping', '-p${DB_PASSWORD}']
      retries: 3

  redis:
    image: redis:alpine
    container_name: redis
    volumes: ['redis_data:/data']
    ports: ['${FORWARD_REDIS_PORT:-6379}:6379']
    networks: [app_net]

  mailpit:                          # atrapa los correos: panel en :8025
    image: axllent/mailpit:latest
    container_name: mailpit
    ports: ['8025:8025']
    networks: [app_net]

Mailpit + Mail::fake, dos redes de seguridad

En pruebas usas Mail::fake() para afirmar que un correo se habría enviado (lo veremos más abajo). En desarrollo manual, Mailpit atrapa esos correos y te los muestra en http://localhost:8025, sin arriesgarte a mandarle nada a un cliente real. Dos capas distintas, el mismo objetivo: que ningún correo se escape por accidente.

El Makefile: tu panel de control autodocumentado

Nadie debería tener que recordar docker compose exec app php artisan migrate --seed. Envuelve cada tarea en un Makefile y deja que el propio archivo se explique con un comando de ayuda:

makefile
.DEFAULT_GOAL := help
DC = docker compose   # Compose v2: "docker compose", no "docker-compose"

help: ## Muestra esta ayuda
    @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / \
    {printf "\033[36m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

start: ## Levanta el entorno
    $(DC) up -d

setup: ## Configura todo: env, contenedores, dependencias, BD y el hook de git
    cp src/.env.example src/.env
    $(DC) up -d
    $(DC) exec -T api sh -c "composer install && php artisan key:generate && php artisan migrate --force --seed"
    cp pre-commit.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit

pint: ## Formatea el código (Laravel Pint)
    $(DC) exec api sh -c "php -d memory_limit=2G vendor/bin/pint"

phpstan: ## Análisis estático (PHPStan / Larastan)
    $(DC) exec api sh -c "php -d memory_limit=2G vendor/bin/phpstan analyze app"

qa: ## Lint + formato (check) + análisis estático
    $(DC) exec api sh -c "vendor/bin/phplint"
    $(DC) exec api sh -c "php -d memory_limit=2G vendor/bin/pint --test"
    $(DC) exec api sh -c "php -d memory_limit=2G vendor/bin/phpstan analyze app"

test: ## Corre las pruebas (Pest, en paralelo)
    $(DC) exec api sh -c "php artisan test --parallel --processes=4 --env=testing"

La magia está en ese awk: recorre el Makefile, busca cada tarea con un comentario ## ... y lo imprime en columnas. Escribir make help te devuelve el menú completo:

txt
$ make help
help               Muestra esta ayuda
start              Levanta el entorno
setup              Configura todo: env, contenedores, dependencias, BD y el hook de git
pint               Formatea el código (Laravel Pint)
phpstan            Análisis estático (PHPStan / Larastan)
qa                 Lint + formato (check) + análisis estático
test               Corre las pruebas (Pest, en paralelo)

¿En Windows? Instala make primero

make viene de serie en Linux y macOS, pero no en Windows. La forma más limpia de tenerlo es con el gestor de paquetes Chocolatey: abre PowerShell como administrador, instala Chocolatey y luego make.

powershell
# 1) Instalar Chocolatey (PowerShell como administrador)
Set-ExecutionPolicy Bypass -Scope Process -Force; `
  iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

# 2) Instalar make
choco install make

Alternativas: Scoop (scoop install make) o trabajar dentro de WSL2, donde tienes un Linux real con make incluido —mi recomendación si desarrollas Laravel en Windows.

Por qué los comandos de ayuda importan tanto

Un Makefile con make help convierte el onboarding de "lee el README de 40 pasos" a "clona, corre make setup y estás dentro". La documentación no se desactualiza porque es el comando que ejecutas. Y como el mismo make test corre en tu máquina y en CI, desaparece la brecha entre "local" y "el servidor".

Poner en marcha el proyecto queda así de corto:

Configura todo de una vez

bash
make setup

Un solo comando copia el .env, construye los contenedores, instala dependencias, genera la clave, migra la base de datos con datos de ejemplo e instala el hook de pre-commit. Todo listo para trabajar.

Verifica que todo está verde

bash
make qa && make test

Lint, formato, análisis estático y pruebas. Si esto pasa en tu máquina, pasará en el runner —es exactamente lo mismo que corre el pipeline.

La primera puerta: el hook de pre-commit

El pre-commit es un guardián que corre en tu máquina justo antes de guardar un commit. Su trabajo no es reemplazar a CI, sino atrapar lo obvio antes de que salga de tu laptop. En el proyecto real corre sobre los .php en el stage, dentro del contenedor, las mismas tres comprobaciones que luego repite el pipeline: sintaxis (php -l), formato (pint --test) y análisis estático (phpstan).

bash
#!/bin/sh
# pre-commit — valida solo los .php en el stage, dentro del contenedor api
SFILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '\.php$' | grep -v '\.blade.php$')
[ -z "$SFILES" ] && exit 0

# convierte las rutas del host (src/...) a las del contenedor (/app/...)
APPPATHS=$(echo "$SFILES" | sed "s:src:/app:g")

echo "→ php -l (sintaxis)…"
for FILE in $SFILES; do
  docker compose exec -T api sh -c "php -l -d display_errors=0 /$(echo "$FILE" | sed 's:src:app:')" || exit 1
done

echo "→ Pint (formato)…"
docker compose exec -T api sh -c "vendor/bin/pint --test $APPPATHS" || exit 1

echo "→ PHPStan (estático)…"
docker compose exec -T api sh -c "php -d memory_limit=1G vendor/bin/phpstan analyze $APPPATHS" || exit 1

Unit, Feature e Integration: los tres niveles de prueba

Antes de escribir pruebas conviene saber qué tipo estás escribiendo, porque cada una responde una pregunta distinta y cuesta distinto. Se suelen agrupar en una pirámide: muchas pruebas baratas en la base, pocas caras en la cima.

Diagrama de la pirámide de pruebas en tres niveles. La base, la más ancha, son las pruebas unitarias: lógica aislada sin framework, muchas, rápidas y baratas (milisegundos). El nivel medio son las pruebas de Feature vía HTTP: casos de uso completos que ejercitan ruta, middleware, controlador y base de datos real. La cima, la más estrecha, son las pruebas de integración y end-to-end: pocas, lentas y caras. Una flecha en el lado izquierdo indica que hacia la base hay más pruebas y hacia la cima menos; otra en el lado derecho indica que hacia la base son más rápidas y baratas y hacia la cima más lentas y caras

Unit (unitarias)

Prueban una pieza aislada —una clase, un método— sin base de datos ni framework. Son rapidísimas (milisegundos). Ideales para lógica pura: cálculos, reglas de negocio, transformaciones. En Laravel viven en tests/Unit.

Feature (de funcionalidad)

Prueban un caso de uso completo entrando por donde entra el usuario: una petición HTTP a un endpoint, con su ruta, middleware, controlador, validación y base de datos real de prueba. Son el corazón de una API. Viven en tests/Feature.

Integration (de integración)

Verifican que varias piezas colaboran bien: tu código con la base de datos, con la cola de trabajos, con un servicio externo. En Laravel muchas veces son un Feature test que además comprueba efectos colaterales (un job encolado, un correo enviado).

La regla práctica

Cubre cada cosa en el nivel más barato que la valide de verdad. ¿Es una fórmula? Unitaria. ¿Es "cuando llega este POST, se crea la factura y se notifica al cliente"? Feature/Integration. No persigas cubrirlo todo con pruebas lentas: es caro y frágil. La pirámide no es dogma, es economía.

Construir el escenario: migraciones, factories y seeds

Una prueba de Feature necesita datos con los que trabajar: un usuario autenticado, unos registros previos, un catálogo. Montar ese escenario a mano sería un infierno. Laravel te da tres herramientas que encajan como piezas de Lego.

Definen el esquema de la base de datos en código versionado. En pruebas, el trait RefreshDatabase las corre antes de cada test y revierte los cambios al terminar, dejando una base limpia cada vez.

php
// database/migrations/xxxx_create_invoices_table.php
Schema::create('invoices', function (Blueprint $table) {
    $table->id();
    $table->string('client');
    $table->decimal('amount', 12, 2);
    $table->string('currency', 3);
    $table->timestamps();
});

Con el escenario armado, tus aserciones se vuelven precisas: puedes comprobar la forma exacta de un JSON que esperas recibir y el valor de un campo en una tabla tras la operación. Justo lo que hace una buena prueba de Feature —y lo que veremos a continuación.

TDD en la práctica: rojo, verde, refactor

TDD suena a disciplina monástica, pero es un bucle de tres pasos que se aprende en una tarde. La regla es simple y contraintuitiva: no escribes código de producción hasta tener una prueba que falle pidiéndolo.

Diagrama del ciclo de TDD en Laravel 13: un bucle de tres fases. ROJO: escribes una prueba para un comportamiento que aún no existe y la ejecutas; falla. VERDE: escribes el código mínimo necesario para que la prueba pase, sin sobre-ingeniería, y la vuelves a ejecutar; pasa. REFACTOR: limpias nombres, eliminas duplicación y aplicas principios SOLID con la red de seguridad de las pruebas verdes. Si falta comportamiento, vuelves al rojo con la siguiente prueba; si no, haces commit y push, donde el hook de pre-commit corre Pint y el lint

Laravel 13 trae Pest 4 como framework de pruebas por defecto (corre sobre PHPUnit 12 por debajo, así que puedes mezclar ambos estilos). Vamos a construir un endpoint POST /api/invoices guiados por las pruebas.

Paso 1 — Rojo: escribe la prueba que falla

Todavía no existe ni la ruta, ni el controlador, ni el modelo. Da igual: describimos el comportamiento que queremos, y de paso comprobamos las dos cosas que importan —el JSON de respuesta y el estado de la base de datos.

php
// tests/Feature/InvoiceTest.php
use function Pest\Laravel\postJson;

it('crea una factura y la persiste', function () {
    $payload = [
        'client'   => 'ACME S.A.C.',
        'amount'   => 1200.50,
        'currency' => 'PEN',
    ];

    postJson('/api/invoices', $payload)
        ->assertCreated()                        // espera 201
        ->assertJsonPath('data.client', 'ACME S.A.C.'); // forma del JSON

    $this->assertDatabaseHas('invoices', [       // valor en la tabla
        'client' => 'ACME S.A.C.',
        'amount' => 1200.50,
    ]);
});

Corre make test y verás el rojo esperado: la ruta no existe (404). Perfecto: la prueba ya está haciendo su trabajo, guiándote hacia lo que falta.

Paso 2 — Verde: el código mínimo para pasar

Ahora escribimos código, pero solo el imprescindible para que la prueba se ponga verde. Nada de anticipar requisitos que nadie pidió.

php
// app/Models/Invoice.php
class Invoice extends Model
{
    protected $fillable = ['client', 'amount', 'currency'];
}
php
// routes/api.php
Route::post('/invoices', StoreInvoiceController::class);
php
// app/Http/Controllers/StoreInvoiceController.php
class StoreInvoiceController
{
    public function __invoke(StoreInvoiceRequest $request)
    {
        $invoice = Invoice::create($request->validated());

        return InvoiceResource::make($invoice)
            ->response()
            ->setStatusCode(201);
    }
}

Corre make test de nuevo y ahora tienes el verde. La prueba pasa: el endpoint crea la factura, responde 201 y persiste los datos.

Paso 3 — Refactor: mejora con la red puesta

Aquí es donde TDD paga su inversión. Con la prueba en verde puedes mejorar el diseño sin miedo: si rompes algo, la prueba te avisa al instante. Movemos la validación a un Form Request (responsabilidad única) en vez de amontonarla en el controlador.

php
// app/Http/Requests/StoreInvoiceRequest.php
class StoreInvoiceRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'client'   => ['required', 'string', 'max:255'],
            'amount'   => ['required', 'numeric', 'min:0'],
            'currency' => ['required', 'string', 'size:3'],
        ];
    }
}

Vuelves a correr los tests: siguen verdes. El comportamiento no cambió, pero el código quedó más limpio y desacoplado. Ese es el ritmo de TDD: pasos pequeños, siempre con la suite verde como brújula.

Para la lógica pura usa pruebas unitarias, que vuelan porque no arrancan el framework:

php
// tests/Unit/MoneyTest.php
it('convierte soles a céntimos sin perder precisión', function () {
    expect(Money::fromDecimal('1200.50', 'PEN')->cents())
        ->toBe(120050);
});

Mocking y fakes: aislar lo que no controlas

¿Qué pasa cuando tu código habla con una pasarela de pago, envía un correo o encola un job? No quieres que tus pruebas cobren tarjetas de verdad ni dependan de que un servicio externo esté en pie. Ahí entran dos técnicas: mocking (sustituir una dependencia por un doble que finge su comportamiento) y los fakes que Laravel trae de fábrica.

Un mock con Mockery te deja definir qué debe devolver una dependencia y verificar que se llamó como esperabas —clave en pruebas de integración donde importa el "cómo", no solo el "qué":

php
it('cobra usando la pasarela de pago', function () {
    // doble de la pasarela: esperamos que charge() se llame una vez
    $gateway = Mockery::mock(PaymentGateway::class);
    $gateway->shouldReceive('charge')
        ->once()
        ->with(120050, 'PEN')
        ->andReturn(new PaymentResult(success: true));

    $this->app->instance(PaymentGateway::class, $gateway);

    postJson('/api/invoices/1/pay')->assertOk();
});

Para las integraciones internas de Laravel —colas, eventos, correos, HTTP saliente— tienes fakes de una línea que interceptan la acción y te dejan afirmar que ocurrió, sin ejecutarla de verdad:

php
use Illuminate\Support\Facades\Queue;

Queue::fake();

// ... acción que debería encolar un job ...

Queue::assertPushed(ProcessInvoice::class);

Verificas que el job se encoló sin necesidad de un worker corriendo.

No abuses del mock

Cada mock acopla la prueba a cómo está implementado tu código, no solo a qué hace. Si mockeas de más, tus pruebas se vuelven frágiles y se rompen en cada refactor. Regla: mockea lo que no controlas o es costoso (pagos, APIs externas, envíos), y deja lo que sí controlas —tu base de datos de prueba— correr de verdad. Una prueba de integración con base real da más confianza que diez con todo simulado.

El pipeline de GitLab CI/CD

Tu suite verde en local es media victoria. La otra mitad es garantizar que nadie integre código que no pase esas mismas puertas, aunque se salte el hook. Ahí entra GitLab CI/CD.

Un GitLab Runner es el proceso que ejecuta tu pipeline: por cada push, levanta contenedores limpios y corre en ellos los trabajos que defines en .gitlab-ci.yml. Al ser contenedores efímeros e idénticos, el resultado no depende de la máquina de nadie.

Diagrama de arquitectura del pipeline de GitLab CI/CD para Laravel 13: el desarrollador hace push al repositorio de GitLab, que dispara el pipeline definido en .gitlab-ci.yml. Un GitLab Runner ejecuta cada job en su propio contenedor, organizado en cuatro stages: 1) setup, que corre composer install y cachea vendor como artefacto; 2) quality, con dos jobs en paralelo, Pint en modo verificación y Larastan; 3) test, que corre Pest con cobertura contra un servicio MySQL efímero; y 4) build, que construye la imagen Docker de la app. La imagen se sube al Container Registry del proyecto y desde ahí se despliega a staging o producción. Las quality gates bloquean el merge: si Pint, Larastan o Pest fallan, el pipeline se detiene en rojo

El pipeline se organiza en stages (etapas) que corren en orden, y dentro de cada una, los jobs pueden correr en paralelo. Míralo por partes:

Instala dependencias una vez (con cd src, porque la app vive ahí) y las comparte con el resto vía cache y artifacts. Cachear src/vendor/ es lo que hace que los pipelines siguientes vuelen.

yaml
setup:
  stage: setup
  script:
    - cd src
    - composer install --optimize-autoloader --prefer-dist --no-interaction --no-scripts
    - cp .env.gitlab .env && cp .env.gitlab .env.testing
    - php artisan key:generate
  artifacts:
    paths: [src/vendor/, src/.env, src/.env.testing]
    expire_in: 1 day
  cache:
    paths: [src/vendor/]
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

El encabezado del archivo ata todo junto —la imagen base (la misma webdevops/php-nginx del entorno local), el orden de las etapas y las variables del servicio de base de datos:

yaml
# .gitlab-ci.yml
image: webdevops/php-nginx:8.3

stages: [setup, quality, testing, report]

variables:
  MYSQL_DATABASE: laravel_test
  MYSQL_ROOT_PASSWORD: secret123

Regla de oro: en CI se verifica, no se arregla

En tu máquina, pint reformatea. En CI, usa siempre pint --test (y --dry-run para herramientas equivalentes): que verifique y falle si algo no cumple, pero que no modifique el código. Un CI que auto-formatea genera commits fantasma que ensucian el historial y confunden el git blame. El arreglo es del desarrollador; el de CI es decir sí o no.

Lo importante no es la sintaxis YAML, sino el principio: son quality gates. Si Pint, Larastan o Pest fallan, el pipeline se detiene en rojo y —si proteges tu rama— el merge request queda bloqueado. El código malo, sencillamente, no entra.

Medir la calidad: SonarQube, cobertura y quality gates

Pint y Larastan cuidan formato y tipos, pero para poner número a la salud del proyecto entra SonarQube (o su versión cloud, SonarCloud). En cada pipeline analiza tu código y calcula justo los indicadores del principio: bugs, vulnerabilidades, code smells, cobertura, duplicación y complejidad (ciclomática y cognitiva). Con esos números estima la deuda técnica en tiempo —cuánto costaría arreglarla— mediante el método SQALE, y le pone a tu proyecto una nota de la A a la E en mantenibilidad, fiabilidad y seguridad.

Añadirlo al pipeline es un job más, que además cruza la cobertura que ya generó Pest:

yaml
sonarqube:
  stage: quality
  needs: [setup]
  image: sonarsource/sonar-scanner-cli:latest
  script: sonar-scanner
  allow_failure: true   # al inicio; luego conviértelo en gate obligatorio

Lo verdaderamente potente es el Quality Gate de SonarQube: una condición que el proyecto debe cumplir para pasar —por ejemplo, "cobertura del código nuevo ≥ 80 %, cero vulnerabilidades nuevas, deuda técnica bajo cierto umbral". Si no se cumple, el merge request se marca en rojo. Es la traducción a política de equipo de todo lo que venimos midiendo.

El 100 % de cobertura no significa correcto

La cobertura mide qué líneas se ejecutan, no si tus aserciones son buenas. Puedes tener 100 % y cero valor si tus pruebas no comprueban nada útil. Persigue cubrir los caminos que importan —lógica de negocio, casos borde, errores— no el número por el número. Un 80 % honesto vale más que un 100 % de fachada. La cobertura es una red de detección de huecos, no una medalla.

Más allá del pipeline: ágil, DevSecOps, DORA y SRE

Todo lo anterior no vive aislado: es la base de cuatro prácticas que definen a los equipos de software de alto rendimiento. Aquí es donde tu inversión en pruebas y calidad se vuelve ventaja de negocio.

Diagrama del ciclo de desarrollo de software confiable, de TDD a DORA, en tres fases conectadas en bucle. Fase 1, Desarrollo (bucle interno rápido): planificar la historia con criterios de aceptación, diseñar con SOLID y arquitectura limpia, TDD en rojo-verde-refactor, y análisis estático local con Pint, Larastan y pre-commit. Fase 2, Integración continua (CI más DevSecOps): build con composer e imagen Docker, pruebas con cobertura en niveles Unit, Feature e Integration, quality gate con SonarQube que mide bugs, code smells y deuda, y seguridad DevSecOps con SAST, SCA y detección de secretos. Fase 3, Entrega y operación (SRE): deploy continuo blue-green o canary, observabilidad con SLI, SLO y error budget, y gestión de incidentes con MTTR y postmortem sin culpa. Al final, las métricas DORA (frecuencia de despliegue, lead time, tasa de fallo del cambio y MTTR) retroalimentan el próximo ciclo de planificación, reforzando el círculo virtuoso

Ágil: Scrum · Lean Startup

Todo empieza por cómo entra el trabajo. Iterar en lotes pequeños (Scrum), validar con el mercado antes de sobre-construir (Lean Startup) y estimar en equipo con Planning Poker produce cambios chicos y frecuentes —justo lo que TDD y CI/CD saben entregar con seguridad. Lotes pequeños = menos riesgo por despliegue y mejor lead time.

DevSecOps

Es "mover la seguridad a la izquierda": integrarla en el pipeline en lugar de dejarla para el final. GitLab trae de serie SAST (análisis estático de seguridad), escaneo de dependencias (SCA, tus librerías vulnerables) y detección de secretos (claves filtradas). Se activan como jobs y suman a las quality gates: una vulnerabilidad nueva bloquea el merge, igual que un test roto.

Métricas DORA

La investigación DORA (Forsgren, Humble y Kim, Accelerate, 2018) identificó cuatro métricas que predicen el rendimiento de un equipo: frecuencia de despliegue, lead time (de commit a producción), tasa de fallo del cambio y tiempo de restauración (MTTR). TDD y CI/CD las mejoran directamente: con una red de pruebas sólida despliegas más seguido, más rápido y rompes menos.

SRE

La Site Reliability Engineering de Google define la fiabilidad con SLI/SLO y error budgets. Un código confiable, bien probado y observable, consume menos "presupuesto de error" y baja el MTTR: cuando algo falla, lo detectas y lo reviertes en minutos. Menos toil, más ingeniería.

El círculo se cierra: mejor código produce mejores métricas DORA, y esas métricas justifican seguir invirtiendo en calidad. No es un coste, es un motor que se refuerza a sí mismo.

SOLID y arquitectura limpia hacen todo esto posible

Quizá notaste algo al refactorizar: mover la validación a un Form Request y las dependencias al constructor (para poder mockearlas) no fue solo estética. Fue lo que mantuvo el código testeable. Y aquí está el círculo virtuoso que cierra el arco:

El código difícil de probar casi siempre es código mal diseñado. Cuando una clase hace demasiado, depende de medio mundo o esconde sus dependencias, escribir una prueba se vuelve un calvario. TDD te empuja, sin sermones, hacia lo contrario: clases con una sola responsabilidad, dependencias inyectadas, funciones pequeñas. Es decir, hacia SOLID y hacia una arquitectura limpia —no porque un libro lo mande, sino porque es la única forma de que las pruebas sean fáciles de escribir.

Por eso la queja del inicio —"hay mucha deuda técnica"— tiene un antídoto concreto. La deuda se acumula cuando cada cambio se hace a ciegas. Cuando cada cambio nace acompañado de su prueba, pasa por análisis estático y cruza un pipeline que lo verifica, la deuda deja de crecer sin control: el diseño mejora a cada paso y la confianza se vuelve la norma.

Cierre

Construir software confiable no es cuestión de talento ni de suerte, sino de un sistema: escribe la prueba primero, mide la calidad con números, automatiza todo el recorrido y cierra el bucle con las métricas de operación. Laravel 13 te da las piezas afiladas —Pest 4 de serie, Pint, Larastan, un ecosistema Docker— y GitLab CI/CD, SonarQube y las prácticas DevSecOps/DORA/SRE las orquestan para que el código malo no llegue nunca a producción.

Empieza pequeño: un Makefile con make help, un hook de pre-commit y tu primera prueba en rojo. En unas semanas, desplegar un viernes por la tarde dejará de dar miedo. Esa tranquilidad tiene nombre: confianza construida a propósito.

¿Quieres llevar esto a tu equipo?

Si tienes una API de Laravel sin pruebas, un pipeline que da más problemas de los que resuelve, o quieres montar TDD, CI/CD y calidad medible desde cero sin frenar la entrega, hablemos. Puedes ver cómo trabajo en la página de servicios.


Fuentes

Libros y artículos académicos

Documentación oficial

Foto de Marco Torres

Escrito por

Marco Torres

Desarrollador Full-Stack senior con DevOps y arquitectura cloud, y Bachiller en Ingeniería de Sistemas. Escribo sobre arquitectura escalable y las lecciones de llevar sistemas a producción — desde la trinchera.