Saltar al contenido principal

Pruebas

Closegram es un monorepo con varias configuraciones de pruebas independientes, un stack por paquete. Esta página es la referencia completa entre paquetes; las convenciones propias de cada paquete están documentadas en su sección correspondiente más abajo.

PaqueteStackEjecutorQué cubre
apps/backendJest (multiproyecto)npm testResolvers de GraphQL, managers, access-services, validators — unit (con mocks) + integration (Postgres real)
apps/frontend-nextjsVitest + React Testing Library, Playwrightnpm test, npm run test:e2ePruebas unitarias de componentes + flujos de extremo a extremo en el navegador
apps/frontend-adminVitest + React Testing Library, Playwrightnpm test, npm run test:e2ePruebas de componentes del kit de UI de admin + flujos de extremo a extremo
packages/uiVitest + React Testing Librarynpm run test -w @repo/uiPrimitivas compartidas del sistema de diseño (comportamiento). Verificación visual (render-smoke) de Storybook mediante test-storybook — consulta Storybook
apps/iosXCTestbundle exec fastlane testSolo la funcionalidad de Auth, organizada por capas según Clean Architecture

El turbo run test de la raíz distribuye la tarea test a través de todos los workspaces (Jest del backend + la ejecución de Vitest de cada app web). E2E (Playwright) y Storybook se invocan por paquete y no forman parte del pipeline test de la raíz. Storybook tiene su propia página: consulta Storybook.


Comandos a nivel de raíz

El package.json de la raíz también define algunos scripts a nivel de todo el repo que están un nivel por encima de los específicos de cada paquete de la tabla de arriba:

ComandoQué hace
npm run testturbo run test — la tarea test propia de cada workspace (Jest del backend + Vitest de cada app web), orquestada por Turbo
npm run test:frontendSolo @repo/ui + frontend-nextjs + frontend-admin (sin backend)
npm run test:backendSolo backend — npm run test:report -w backend (la ejecución de Jest al estilo CI, con heap más grande + --workerIdleMemoryLimit=512MB)
npm run test:allLa suite completa: test:frontend y luego test:backend, canalizado a través de tee hacia test-results.log en la raíz del repo (para que la salida de una ejecución completa quede disponible para inspección después, no solo visible en el scrollback de la terminal)
npm run test:logturbo run test --output-logs=new-only, también canalizado con tee hacia test-results.log — una variante más liviana de test:all que pasa por Turbo en vez de un pipeline de shell frontend-luego-backend

npm run test:all es el comando que hay que ejecutar antes de dar por terminada cualquier tarea de desarrollo de funciones (consulta el CLAUDE.md de la raíz) — es el único de estos que cubre frontend y backend en una sola ejecución.

El requisito de TZ=UTC

La suite unitaria del backend congela el reloj del sistema en varias pruebas (p. ej. apps/backend/tests/unit-test/payments-subscriptions.unit.test.js, mediante jest.useFakeTimers() + jest.setSystemTime(...)) bajo el supuesto de que la zona horaria del contenedor es UTC — el mismo supuesto bajo el que corren CI y producción. Ejecutar la suite en una máquina cuya zona horaria local no sea UTC puede desplazar las aserciones que dependen de límites de fecha (p. ej. "¿esta marca de tiempo sigue siendo hoy") y producir fallos que no tienen nada que ver con el cambio que estás probando.

Exporta siempre TZ=UTC antes de ejecutar las pruebas del backend localmente en una máquina que no esté en UTC:

TZ=UTC npm run test:all # o npm test / npm run test:unit desde apps/backend

La propia herramienta de este repo ya hace esto por ti en un lugar: el Stop hook que Claude Code ejecuta al terminar un turno invoca TZ=UTC npm run test:all exactamente por este motivo. Cualquier otra automatización o pipeline de CI que ejecute la suite del backend debería hacer lo mismo.


Backend — Jest (apps/backend)

El backend usa una única instalación de Jest con una configuración multiproyecto (apps/backend/jest.config.js) que divide las pruebas en dos proyectos — unit e integration — que se ejecutan de forma secuencial (maxWorkers: 1, para evitar conflictos de base de datos entre los conjuntos de integración).

ProyectotestMatchArchivo de setupTiempo límite¿Usa base de datos?
unittests/unit-test/**/*.test.jstests/setup.js10 000 msNo — todo está simulado con mocks
integrationtests/integration/**/*.test.jstests/integration/setup.integration.js30 000 msSí — Postgres real mediante Sequelize

Estructura de directorios

apps/backend/
├── jest.config.js
├── scripts/test.sh # full lifecycle wrapper (docker up → migrate → jest → teardown)
├── .env.test # gitignored test DB/Redis creds (must exist locally)
├── docker-compose.yml # postgres-test / redis-test services (profile: test)
└── tests/
├── setup.js # unit-project setup — mocks everything
├── helpers/
│ ├── apollo-server-helper.js # builds a real Apollo Server for integration tests
│ └── test-data-generator.js # TestDataGenerator — unique users/emails/usernames per call
├── unit-test/ # *.unit.test.js (managers, access-services, validators, utils)
│ └── resolvers/ # *.resolver.test.js, one per GraphQL resolver
│ └── user-resolver/ # large resolvers split into topic files (+ a local setup.js)
└── integration/ # *.integration.test.js — DB-backed suites
├── setup.integration.js # loads .env.test, opens/closes the Sequelize connection
└── user-resolvers/ # user.resolver integration suite, split by topic (+ setup.js)

Convención de nombres. Las pruebas unitarias terminan en .unit.test.js (las pruebas unitarias de resolvers terminan en .resolver.test.js / .test.js dentro de resolvers/); las pruebas de integración terminan en .integration.test.js. Haz coincidir el módulo bajo prueba con un archivo de prueba del mismo nombre — por ejemplo, message.resolver.jstests/unit-test/resolvers/message.resolver.test.js y tests/integration/message.integration.test.js. Los resolvers grandes (por ejemplo, user.resolver.js) se dividen en archivos organizados por tema dentro de una subcarpeta <name>-resolver/.

Pruebas unitarias

Las pruebas unitarias simulan con mocks la capa de manager / access-service / base de datos y llaman directamente a los resolvers (o managers) — nada por debajo del mock se ejecuta realmente, así que no hay base de datos. El tests/setup.js global ya simula la superficie compartida (pubsub.service, firebase.service, s3.service, los servicios de notificaciones, database/models, data-access-services, el manager/validator de usuario admin, translation.service, y el middleware permissions.requireAuth), así que la mayoría de los archivos heredan esos mocks y solo sobrescriben lo que necesitan en cada archivo.

Patrón: usa jest.mock(...) para las dependencias al inicio del archivo, jest.clearAllMocks() en beforeEach, construye un mockContext ({ user: { userId, username }, lng/lang }), y luego llama directamente a resolvers.Query.x(...) / resolvers.Mutation.x(...). Si un resolver accede directamente a un access-service (y no solo a su manager), ese access-service también debe simularse con un mock, o la prueba terminará golpeando un modelo real de Sequelize.

Para verificar que una operación requiere autenticación, establece mockContext.user = null y espera un error 'authentication.required'.

Pruebas de integración

Las pruebas de integración ejercitan la ruta completa — GraphQL → resolver → manager → access-service → Postgres. Construyen un Apollo Server real (sin enrutar) a partir del schema real mediante createUserTestServer() / createAdminTestServer() (tests/helpers/apollo-server-helper.js), lo ejecutan con createExecuteWithAuth(server) (envuelve server.executeOperation, agregando un token Bearer / encabezado Accept-Language y desempaquetando la forma de respuesta de Apollo v4 a { data, errors }), y usan managers/access-services reales contra la base de datos de prueba.

Registra datos con userManager.register(...) + TestDataGenerator.user() (username/email únicos en cada llamada para evitar colisiones en la base de datos compartida, que no se reinicia entre pruebas), y luego limpia las filas creadas en afterAll mediante los métodos deleteAll/destroyAll/delete del access-service correspondiente. La conexión de Sequelize en sí se cierra globalmente en setup.integration.js, no por archivo. Los archivos setup.js locales de cada directorio (por ejemplo, tests/integration/user-resolvers/setup.js) contienen los helpers compartidos createTestUser/createAdminUser/cleanupTestData — revisa si ya existe uno antes de agregar nuevos fixtures.

Base de datos de prueba / Docker

Las pruebas de integración necesitan un Postgres y un Redis reales. Son contenedores dedicados de prueba (definidos en apps/backend/docker-compose.yml bajo profiles: [test]), deliberadamente separados de los servicios postgres/redis de desarrollo para que las pruebas nunca toquen datos de desarrollo:

ServicioContenedorImagenBase de datos por defectoPuerto por defecto
postgres-testclosegram-postgres-testpostgres:15-alpineclosegram_test5433
redis-testclosegram-redis-test6380

Las credenciales provienen de apps/backend/.env.test (ignorado por git — debe existir localmente).

npm test (y test:unit / test:integration) ejecutan el ciclo de vida completo mediante scripts/test.sh: inicia los contenedores → wait_for_postgres (consulta pg_isready, 30 intentos / 2 s) → wait_for_redis (consulta redis-cli ping, 20 intentos / 2 s) → NODE_ENV=test npx sequelize-cli db:migrate → Jest (con --forceExit, con salida duplicada mediante tee a logs/test-<timestamp>.log, con enlace simbólico logs/test-latest.log) → siempre derriba los contenedores y sus volúmenes con nombre mediante un trap de EXIT, ya sea con éxito o con fallo. Todos los demás scripts asumen que la base de datos de prueba ya está activa.

Para levantar la base de datos manualmente (para el modo watch, cobertura, o ejecuciones repetidas):

cd apps/backend
npm run docker:up:test # docker compose --env-file .env.test --profile test up -d postgres-test redis-test
npm run migrate:test # cross-env NODE_ENV=test npx sequelize-cli db:migrate
# or both at once:
npm run test:db:setup # docker up + sleep 3 + migrate
# when done:
npm run test:db:teardown

Otros helpers de base de datos: migrate:test:undo, migrate:test:undo:all, seed:test, db:test:reset (undo-all + migrate).

Referencia de comandos del backend

Ejecuta desde apps/backend/:

ComandoQué hace
npm testCiclo de vida completo (docker up → migrate → todos los proyectos de Jest → teardown)
npm run test:unitMismo ciclo de vida, --selectProjects unit
npm run test:integrationMismo ciclo de vida, --selectProjects integration --forceExit
npm run test:unit (iterar)Rápido, sin base de datos — úsalo para la mayoría de las iteraciones
npm run test:watchjest --watch (sin wrapper de ciclo de vida — la base de datos ya debe estar activa)
npm run test:integration:watchjest --selectProjects integration --watch (sin wrapper de ciclo de vida)
npm run test:coverage / :unit / :integrationIgual que lo anterior con --coverage; no levanta Docker/migrate por sí mismo — inicia la base de datos primero para la parte de integración
npm run test:debugnode --inspect-brk … jest --runInBand
npm run test:reportEjecución unitaria con un heap más grande + --workerIdleMemoryLimit=512MB (al estilo CI)

Solo test, test:unit y test:integration gestionan Docker/migraciones automáticamente; todo lo demás asume que la base de datos de prueba ya está en ejecución.

Cobertura del backend

  • Se recolecta de: graphql/resolvers/, managers/, validators/, services/, data-access-services/ (excluye node_modules, coverage, tests). La capa de modelo y el schema están excluidos — son mayormente declarativos.
  • Salida: apps/backend/coverage/. Reporters: text (terminal), lcov, y html (reporte navegable).
  • Umbrales (globales): statements: 20, branches: 15, functions: 20, lines: 20. Son un piso deliberadamente conservador para "no dejar que caiga silenciosamente a cero" (así lo indica jest.config.js), no un objetivo real — la idea es irlos subiendo una vez que se midan números reales con npm run test:coverage.
  • verbose: true está configurado globalmente. Un reporte opcional de jest-html-reporters (./test-report/index.html, "Closegram Backend Tests") se activa solo si ese paquete está instalado — la configuración lo verifica con require.resolve y lo omite silenciosamente en caso contrario.

También existe una verificación estática de desincronización no relacionada con Jest: npm run check:schema compara los campos del schema con los resolvers exportados, sin necesidad de base de datos ni de un test runner. Consulta Arquitectura del Backend.


Apps web — Vitest + Playwright (apps/frontend-nextjs, apps/frontend-admin)

Ambas apps de Next.js comparten un stack de pruebas idéntico — Vitest + React Testing Library para pruebas unitarias/de componentes y Playwright para e2e — configurado de forma casi idéntica, con algunas diferencias específicas de cada app.

Pruebas unitarias (Vitest)

Configurado en el vitest.config.ts de cada app: environment: 'jsdom', globals: true, setupFiles: ['./vitest.setup.ts'], y el glob include: ['src/**/*.test.{ts,tsx}'] (excluyendo node_modules, e2e/, y *.stories.tsx). Los archivos de prueba están co-ubicados junto al componente que prueban — no hay un directorio __tests__/ separado.

Diferencias específicas de cada app:

frontend-nextjsfrontend-admin
Alias de ruta@src, más @/lib/firebasesrc/test/mocks/firebase.ts@src
Polyfills adicionales de jsdom (vitest.setup.ts)window.matchMedia, Element.prototype.animate, Element.prototype.scrollIntoViewwindow.matchMedia, ResizeObserver
Wrapper de test-utilsrenderWithProviders (Apollo MockedProvider + Theme + Toast, Auth opcional)renderWithIntl (NextIntlClientProvider con un pequeño conjunto testMessages en español)
Archivos de pruebas unitarias (actual)8 (src/components/**)11 (mayormente src/components/ui/**)

El alias de Firebase en frontend-nextjs significa que cualquier componente que importe el cliente real de Firebase obtiene de forma transparente src/test/mocks/firebase.ts en cada prueba unitaria — no hace falta simular con mocks por prueba. Ninguna de las dos apps configura cobertura en vitest.config.ts — hoy en día la recolección de cobertura no está habilitada en ninguna de las dos apps web.

Usa el wrapper renderWith* de la app para componentes que dependen de muchos providers; el render simple de RTL está bien cuando un componente no necesita providers. Consulta por rol/nombre accesible, no por detalles de implementación:

import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '@/test/test-utils'; // frontend-nextjs
import { ThemeToggle } from './ThemeToggle';

it('flips the aria-label after being clicked', async () => {
renderWithProviders(<ThemeToggle />);
const btn = screen.getByRole('button');
const before = btn.getAttribute('aria-label');
await userEvent.click(btn);
expect(btn.getAttribute('aria-label')).not.toBe(before);
});

Pruebas de extremo a extremo (Playwright)

Los specs viven en e2e/*.spec.ts de cada app. Ambas configuraciones usan un único proyecto chromium (sin Firefox/WebKit), reintentan solo en CI, capturan un trace en el primer reintento, y arrancan la app automáticamente con npm run dev a menos que se establezca E2E_BASE_URL — lo que te permite apuntar Playwright a un servidor + backend ya en ejecución.

frontend-nextjsfrontend-admin
baseURL / puerto de devhttp://localhost:3000http://localhost:3100
Reportergithub (CI) / html (local)list
Specssmoke, auth, authenticated, public-pagessmoke, auth, authenticated

La app de admin no tiene un spec public-pages (no tiene páginas públicas de marketing). En cuanto a estilo, las pruebas smoke de frontend-nextjs verifican una redirección directa a /login; las de admin están escritas de forma más defensiva (status < 500, "ruta de login O campo de contraseña presente") ya que su flujo de login puede renderizarse en distintas URLs.

Apunta e2e a un stack que ya esté en ejecución:

E2E_BASE_URL=http://localhost:3000 npm run test:e2e -w frontend-nextjs
E2E_BASE_URL=http://localhost:3100 npm run test:e2e -w frontend-admin

Referencia de comandos web

Los scripts son idénticos en ambas apps (test, test:watch, test:e2e, test:e2e:ui, test:ui). Ejecútalos desde la raíz del repo con -w, o entra primero a la app con cd:

ComandoQué hace
npm run test -w frontend-nextjsEjecución única de Vitest (al estilo CI)
npm run test:watch -w frontend-nextjsModo watch de Vitest
npm run test:ui -w frontend-nextjsUI de Vitest
npm run test:e2e -w frontend-nextjsPlaywright (arranca automáticamente next dev --port 3000)
npm run test:e2e:ui -w frontend-nextjsModo UI de Playwright

Sustituye frontend-nextjs por frontend-admin (su e2e arranca automáticamente en el puerto 3100). A nivel de todo el repo: npm run test (turbo run test, todos los workspaces incl. backend) o npm run test:frontend (@repo/ui + ambas apps). No existe un pipeline de e2e a nivel de raíz — Playwright se invoca por app.


Biblioteca de UI compartida — Vitest (packages/ui)

@repo/ui tiene su propia instalación de Vitest para pruebas de comportamiento de las primitivas del sistema de diseño (vitest.config.ts: jsdom, globals: true, include: ['src/**/*.test.{ts,tsx}']). Hay 15 archivos de prueba — 13 componentes *.test.tsx (uno por cada componente de Primitives/*) más 2 de lógica simple *.test.ts (src/styles/boxes.test.ts, src/utils/cx.test.ts) — cada uno co-ubicado junto a la implementación del componente y su .stories.tsx (por ejemplo, src/Button/{Button.tsx, Button.stories.tsx, Button.test.tsx}).

Las pruebas usan React Testing Library + @testing-library/user-event + Vitest, verificando el resultado del render y el comportamiento (manejadores de clic, estado deshabilitado, renderizado condicional) mediante consultas accesibles:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

it('fires onClick when clicked', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});

Las pruebas de Vitest y las stories de Storybook son complementarias: *.test.tsx cubre el comportamiento; *.stories.tsx cubre los estados visuales, y — solo en packages/ui — recibe una pasada automatizada de verificación visual (render-smoke) mediante test-storybook. Consulta Storybook para ese ejecutor.

npm run test -w @repo/ui # vitest run
npm run test:watch -w @repo/ui # vitest (watch)
npm run test:ui -w @repo/ui # vitest --ui

iOS — XCTest (apps/ios)

La app de iOS usa XCTest (no Swift Testing). El target appTests es un PBXNativeTarget real (bundle de pruebas unitarias com.closegram.appTests) integrado en el scheme app, así que la suite se puede compilar y ejecutar desde Xcode. No hay un target de pruebas de UI.

Las pruebas reflejan las capas de Clean Architecture de la app y están acotadas solo a la funcionalidad de Authapps/ios/app/appTests/Tests/AuthTests/:

AuthTests/
├── Data/MockAuthRepository.swift # hand-rolled protocol mock (call-count/arg tracking, configurable success/failure/delay)
├── Domain/UseCases/ # LoginUseCaseTests, RegisterUseCaseTests
└── Presentation/
├── Store/ # AuthReducerTests (pure state transitions), AuthStoreTests (async + Combine publisher)
└── ViewModels/ # Login / Register / ForgotPassword / Password view-model tests

Eso equivale a aproximadamente 156 métodos de prueba que cubren login, registro, OTP por teléfono, login social, restablecimiento/cambio de contraseña, y logout en todas las capas (validación de casos de uso, transiciones del reducer al estilo Redux, el AuthStore asíncrono y su statePublisher de Combine, y la lógica de presentación de los view-models). Ninguna otra área de funcionalidad (Feed, Profile, Messaging, …) tiene pruebas todavía — esta es una suite de una sola funcionalidad, no de toda la app. La propia documentación del repo lo reconoce: apps/ios/README.md lista la cobertura de Repository como "⏳ Pending" y la tabla de estado del proyecto dice "Testing: 0%". No hay pruebas de integración contra un backend/capa de GraphQL real ni pruebas de UI/snapshot.

Cómo ejecutar

# Xcode: open app/app.xcodeproj, select the `app` scheme, ⌘U

# Fastlane (matches CI) — from apps/ios
bundle install # first time
SKIP_GIT_CHECK=true bundle exec fastlane test # local/simulator run, HTML + JUnit output
bundle exec fastlane test_ci # CI lane (simulator, no device UUID needed)
bundle exec fastlane test_coverage # test + xcov report (70% min gate)

# Wrapper script (thin — calls fastlane test / test_coverage)
apps/ios/scripts/test.sh [--coverage]

# Raw xcodebuild
xcodebuild test \
-workspace apps/ios/app/app.xcodeproj/project.xcworkspace \
-scheme app \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro'

El lane test de fastlane tiene codificado un destino de dispositivo físico (id=00008120-000E644C1ED2201E). Sin ese iPhone exacto, usa test_ci en su lugar — omite destination y deja que Xcode elija un simulador.