Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/midudev/jscamp/llms.txt

Use this file to discover all available pages before exploring further.

Este módulo configura un monorepo listo para CI/CD con tests automatizados para backend y frontend. Usa pnpm workspaces para gestionar los sub-paquetes y Vitest como test runner en ambos extremos, combinado con Supertest para tests HTTP en el backend y React Testing Library para tests de componentes en el frontend.

Lo que aprenderás

  • Configurar un monorepo con pnpm workspaces
  • Escribir tests HTTP de integración con Supertest
  • Usar Vitest como alternativa rápida a Jest, compatible con ESM
  • Testear componentes React con React Testing Library y jsdom
  • Linting con ESLint en todos los paquetes con un solo comando
  • Añadir un badge de GitHub Actions al README

Estructura del Monorepo

09-ci-cd/
├── package.json                  ← scripts del monorepo (pnpm workspaces)
├── pnpm-workspace.yaml
└── projects/
    ├── backend/                  ← @jscamp-ci/backend
    │   ├── package.json
    │   ├── src/
    │   │   ├── app.js            ← factory de Express app
    │   │   ├── tasks.js          ← lógica de tareas y validación
    │   │   └── server.js         ← punto de entrada HTTP
    │   └── test/
    │       └── app.test.js       ← tests con Vitest + Supertest
    └── frontend/                 ← @jscamp-ci/frontend
        ├── package.json
        ├── src/
        │   ├── App.jsx           ← gestor de tareas en React
        │   ├── App.test.jsx      ← tests con React Testing Library
        │   └── tasks.js
        └── vite.config.js

Tests del Backend

El backend expone una Task API construida con Express 5. Los tests usan el patrón factory function (createApp) para crear instancias de la app aisladas en cada test, sin compartir estado entre suites.
import request from "supertest";
import { describe, expect, it } from "vitest";
import { createApp } from "../src/app.js";

const app = createApp();

describe("Task API", () => {
  it("documents available endpoints from the root route", async () => {
    const response = await request(app).get("/").expect(200);

    expect(response.body).toMatchObject({
      name: "Task API",
      description: expect.any(String),
    });
    expect(response.body.endpoints).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ method: "GET", path: "/health" }),
        expect.objectContaining({ method: "GET", path: "/api/tasks" }),
        expect.objectContaining({ method: "POST", path: "/api/tasks" }),
      ]),
    );
  });

  it("returns health information", async () => {
    const response = await request(app).get("/health").expect(200);

    expect(response.body).toMatchObject({
      ok: true,
      service: "task-api",
    });
    expect(response.body.timestamp).toEqual(expect.any(String));
  });

  it("lists tasks", async () => {
    const response = await request(app).get("/api/tasks").expect(200);

    expect(response.body.tasks).toHaveLength(2);
    expect(response.body.tasks[0]).toMatchObject({
      id: "task-1",
      title: "Comprar café",
    });
  });

  it("returns 404 for unknown tasks", async () => {
    const response = await request(app).get("/api/tasks/unknown").expect(404);
    expect(response.body).toEqual({ error: "Task not found" });
  });

  it("creates tasks", async () => {
    const response = await request(app)
      .post("/api/tasks")
      .send({ title: "Leer documentación", priority: "low" })
      .expect(201);

    expect(response.body.task).toMatchObject({
      title: "Leer documentación",
      priority: "low",
      completed: false,
    });
  });

  it("validates task title", async () => {
    const response = await request(app)
      .post("/api/tasks")
      .send({ title: "no" })
      .expect(400);

    expect(response.body.error).toBe("title must contain at least 3 characters");
  });

  it("updates tasks", async () => {
    const response = await request(app)
      .patch("/api/tasks/task-1")
      .send({ completed: true })
      .expect(200);

    expect(response.body.task.completed).toBe(true);
  });

  it("returns task stats", async () => {
    const isolatedApp = createApp({
      initialTasks: [
        { id: "task-1", title: "Comprar pan", priority: "low", completed: true },
        { id: "task-2", title: "Llamar a Ana", priority: "high", completed: false },
      ],
    });

    const response = await request(isolatedApp).get("/api/stats").expect(200);

    expect(response.body.stats).toEqual({
      total: 2,
      completed: 1,
      pending: 1,
    });
  });

  it("deletes tasks", async () => {
    const isolatedApp = createApp({
      initialTasks: [
        { id: "task-1", title: "Comprar pan", priority: "low", completed: false }
      ],
    });

    await request(isolatedApp).delete("/api/tasks/task-1").expect(204);

    const response = await request(isolatedApp).get("/api/tasks").expect(200);
    expect(response.body.tasks).toEqual([]);
  });
});

Casos de test cubiertos

TestMétodoRutaQué verifica
Root routeGET/Devuelve nombre, descripción y lista de endpoints
Health checkGET/healthok: true, service: "task-api" y timestamp
Listar tareasGET/api/tasksArray de 2 tareas con id y title correctos
Tarea no encontradaGET/api/tasks/unknownHTTP 404 con { error: "Task not found" }
Crear tareaPOST/api/tasksHTTP 201, tarea con completed: false
Validación de títuloPOST/api/tasksHTTP 400 si el título tiene menos de 3 caracteres
Actualizar tareaPATCH/api/tasks/:idModifica completed o title de la tarea
EstadísticasGET/api/stats{ total, completed, pending } calculados correctamente
Eliminar tareaDELETE/api/tasks/:idHTTP 204 y tarea eliminada de la lista
Los tests de estadísticas y eliminación usan createApp({ initialTasks }) para inyectar datos conocidos, evitando dependencias entre tests. Es el patrón recomendado para tests deterministas.

Tests del Frontend

El frontend usa React Testing Library junto con Vitest para renderizar componentes en un entorno DOM simulado (jsdom).
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { App } from "./App";

describe("App", () => {
  it("renders the task planner", () => {
    render(<App />);

    expect(
      screen.getByRole("heading", { name: /plan del día/i })
    ).toBeInTheDocument();

    expect(
      screen.getByLabelText(/nueva tarea/i)
    ).toBeInTheDocument();

    expect(
      screen.getByText(/preparar ejemplos de github actions/i)
    ).toBeInTheDocument();

    expect(
      screen.getByText("GET /api/tasks")
    ).toBeInTheDocument();
  });
});

¿Qué verifica este test?

El test renderiza el componente <App /> y comprueba que:
  • El heading principal “Plan del día” está presente en el DOM.
  • El campo de nueva tarea (identificado por su label accesible) está visible.
  • Una de las tareas iniciales del seed (“preparar ejemplos de github actions”) se renderiza en la lista.
  • La sección informativa con GET /api/tasks aparece en la página.
React Testing Library promueve los queries por rol y texto (getByRole, getByLabelText, getByText) en lugar de por clase o id, lo que hace que los tests sean más cercanos a cómo el usuario real percibe la interfaz.

Scripts del Monorepo

El package.json raíz define scripts que usan la flag -r (recursive) de pnpm para ejecutar el mismo script en todos los paquetes del workspace simultáneamente:
{
  "name": "09-ci-cd",
  "private": true,
  "type": "module",
  "scripts": {
    "dev:frontend":   "pnpm --filter @jscamp-ci/frontend dev",
    "dev:backend":    "pnpm --filter @jscamp-ci/backend dev",
    "lint:frontend":  "pnpm --filter @jscamp-ci/frontend lint",
    "lint:backend":   "pnpm --filter @jscamp-ci/backend lint",
    "lint":           "pnpm -r lint",
    "test:frontend":  "pnpm --filter @jscamp-ci/frontend test",
    "test:backend":   "pnpm --filter @jscamp-ci/backend test",
    "test":           "pnpm -r test",
    "build:frontend": "pnpm --filter @jscamp-ci/frontend build",
    "build:backend":  "pnpm --filter @jscamp-ci/backend build",
    "build":          "pnpm -r build",
    "start:backend":  "pnpm --filter @jscamp-ci/backend start"
  }
}
Script raízEquivalenteDescripción
testpnpm -r testEjecuta vitest run en backend y frontend
lintpnpm -r lintEjecuta ESLint en todos los paquetes
buildpnpm -r buildCompila frontend (Vite) y verifica sintaxis del backend
test:backendpnpm --filter @jscamp-ci/backend testSolo tests del backend
test:frontendpnpm --filter @jscamp-ci/frontend testSolo tests del frontend

Ejecutar Tests

1

Instalar dependencias

cd 09-ci-cd
pnpm install
2

Ejecutar todos los tests

pnpm test
# Equivale a: pnpm -r test
3

Solo tests del backend

pnpm --filter @jscamp-ci/backend test
4

Solo tests del frontend

pnpm --filter @jscamp-ci/frontend test
5

Modo watch (desarrollo)

# Backend
pnpm --filter @jscamp-ci/backend test:watch
# Frontend
pnpm --filter @jscamp-ci/frontend test:watch
Añade un workflow de GitHub Actions para ejecutar los tests automáticamente en cada push y pull request:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 10
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'
      - run: pnpm install
      - run: pnpm test
      - run: pnpm lint
Una vez configurado, puedes añadir el badge de estado al README:
![CI](https://github.com/tu-usuario/tu-repo/actions/workflows/ci.yml/badge.svg)

Build docs developers (and LLMs) love