Los ejecutores de pruebas heredados (como Jest combinado con Babel o Webpack) dedicaban un porcentaje sustancial del ciclo de CPU a precompilaciones, empaquetado y transformaciones intermedias entre CommonJS y ESM. En la arquitectura de Vitest, el motor de ejecución se integra directamente dentro del servidor de desarrollo de Vite.
La arquitectura de Vitest se fundamenta en tres pilares esenciales:
vite.config.ts), plugins (Rollup/Vite) y alias de rutas.import y require, problemas de dependencias circulares y fallos con import.meta.+-------------------------------------------------------------------------+
| Vitest Main Process |
| +---------------------------+ +-------------------------------+ |
| | Vite Dev Server (HMR) | <---> | Config & Module Graph Resolver| |
| +---------------------------+ +-------------------------------+ |
| | |
| v |
| +-------------------------------------------------------------------+ |
| | Pool Manager (tinypool / forks) | |
| +-------------------------------------------------------------------+ |
+-------------------------------------------------------------------------+
|
+-----------------------+-----------------------+
| |
v v
+-------------------------------+ +-------------------------------+
| Worker 1 (worker_threads) | | Worker N (worker_threads) |
| +-------------------------+ | | +-------------------------+ |
| | V8 Context / Environment| | | | V8 Context / Environment| |
| | (happy-dom / jsdom/node)| | | | (happy-dom / jsdom/node)| |
| +-------------------------+ | | +-------------------------+ |
| | Test Suite: auth.test.ts| | | | Test Suite: user.test.ts| |
| +-------------------------+ | | +-------------------------+ |
| | Vitest Runtime Runner | | | | Vitest Runtime Runner | |
| +-------------------------+ | | +-------------------------+ |
+-------------------------------+ +-------------------------------+--watch): Al apoyarse en el grafo de dependencias de Vite, Vitest invalida y reejecuta exclusivamente los archivos de test que importan, directa o transitivamente, el código modificado.bench()) bajo la misma infraestructura y sintaxis que las pruebas convencionales..snap periféricos.vitest.config.ts#Vitest 4+ ofrece control minucioso sobre los pools de ejecución, tiempos límite de espera, niveles de aislamiento y motores de cobertura.
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths()],
define: {
// Erradicación de tests in-source durante la compilación a producción
'import.meta.vitest': 'undefined',
},
test: {
// Habilitar detección de tests in-source
includeSource: ['src/**/*.{ts,tsx}'],
// Elección de pool: 'threads' | 'forks' | 'vmThreads'
pool: 'threads',
poolOptions: {
threads: {
maxThreads: 8,
minThreads: 2,
isolate: true, // Aislar contexto V8 entre suites de test
useAtomics: true, // Utilizar Atomics y SharedArrayBuffer para sincronización
},
},
// Entorno por defecto para tests de DOM
environment: 'happy-dom',
// Timeout por test (ms)
testTimeout: 5000,
// Motor nativo de cobertura V8 (sin instrumentación AST pesada de Babel)
coverage: {
provider: 'v8',
reporter: ['text', 'json-summary', 'lcov'],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
},
// Ejecutor de benchmarks
benchmark: {
include: ['src/**/*.bench.{ts,tsx}'],
reporters: ['default'],
},
},
});El testing in-source permite mantener las pruebas unitarias de funciones privadas y utilidades matemáticas dentro del mismo archivo fuente, evitando la proliferación de microarchivos de test dispersos por el repositorio.
Al compilar para producción, el bloque if (import.meta.vitest) es eliminado íntegramente mediante DCE (Dead Code Elimination).
/**
* Validación de números de tarjetas de crédito y cuentas mediante el algoritmo de Luhn.
*/
export function isValidLuhn(cardNumber: string): boolean {
const sanitized = cardNumber.replace(/\D/g, '');
if (sanitized.length < 13 || sanitized.length > 19) {
return false;
}
let sum = 0;
let shouldDouble = false;
for (let i = sanitized.length - 1; i >= 0; i--) {
const char = sanitized.charAt(i);
let digit = parseInt(char, 10);
if (Number.isNaN(digit)) {
return false;
}
if (shouldDouble) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
shouldDouble = !shouldDouble;
}
return sum % 10 === 0;
}
// Suite in-source: ejecutada por Vitest, eliminada en la build de producción
if (import.meta.vitest) {
const { describe, it, expect } = import.meta.vitest;
describe('isValidLuhn (In-source Test Suite)', () => {
it('confirma la validez de un número de prueba conforme a Luhn', () => {
// Número válido según estándar Luhn
expect(isValidLuhn('49927398716')).toBe(true);
});
it('rechaza numeraciones con suma de control inválida', () => {
expect(isValidLuhn('49927398717')).toBe(false);
});
it('descarta cadenas que no cumplen con los límites de longitud', () => {
expect(isValidLuhn('12345')).toBe(false);
expect(isValidLuhn('')).toBe(false);
});
it('ignora limpiamente espacios y guiones separadores', () => {
expect(isValidLuhn('4992-7398-716')).toBe(true);
expect(isValidLuhn('4992 7398 716')).toBe(true);
});
});
}Para que TypeScript resuelva correctamente los tipos de import.meta.vitest, registra las definiciones de tipos:
/// <reference types="vite/client" />
/// <reference types="vitest/importMeta" />Los archivos externos de snapshots (.snap) generan habitualmente fatiga cognitiva: ante cualquier fallo, los desarrolladores ejecutan ciegamente vitest -u sin analizar diffs kilométricos. Un Inline Snapshot mantiene el resultado esperado como argumento del método toMatchInlineSnapshot(), convirtiendo la prueba en una especificación autodocumentada.
import { describe, it, expect } from 'vitest';
interface AuditLog {
eventId: string;
timestamp: number;
actor: {
id: string;
role: 'admin' | 'editor';
};
payload: Record<string, unknown>;
}
function createAuditRecord(actorId: string, role: 'admin' | 'editor', action: string): AuditLog {
return {
eventId: `evt_${crypto.randomUUID()}`,
timestamp: Date.now(),
actor: { id: actorId, role },
payload: { action, ip: '127.0.0.1' },
};
}
describe('createAuditRecord', () => {
it('genera una estructura de auditoría determinista con campos dinámicos', () => {
const record = createAuditRecord('usr_42', 'admin', 'UPDATE_ROLE');
// Empleo de Property Matchers para aislar valores no deterministas (UUID, marcas de tiempo)
expect(record).toMatchInlineSnapshot(
{
eventId: expect.stringMatching(/^evt_[0-9a-f-]+$/),
timestamp: expect.any(Number),
},
`
{
"actor": {
"id": "usr_42",
"role": "admin",
},
"eventId": StringMatching /^evt_[0-9a-f-]+$/,
"payload": {
"action": "UPDATE_ROLE",
"ip": "127.0.0.1",
},
"timestamp": Any<Number>,
}
`
);
});
});Vitest incluye capacidad de pruebas de rendimiento a través de su API nativa bench(), facilitando la comparación algorítmica sin incorporar herramientas externas.
import { bench, describe } from 'vitest';
describe('Búsqueda de elementos: Array.includes vs Set.has', () => {
const sampleSize = 10_000;
const targetId = 'item-9999';
const rawArray: string[] = Array.from({ length: sampleSize }, (_, i) => `item-${i}`);
const lookupSet: Set<string> = new Set(rawArray);
bench('Array.includes (Complejidad lineal O(N))', () => {
rawArray.includes(targetId);
});
bench('Set.has (Complejidad constante amortizada O(1))', () => {
lookupSet.has(targetId);
});
});La ejecución mediante npx vitest bench genera un informe detallado con operaciones por segundo (ops/sec), latencia media y márgenes de error estadístico.
Desactivar el aislamiento de workers (isolate: false) agiliza los tiempos de ejecución, pero provoca que mutaciones sobre window, document, mocks globales o singletons se filtren a otros archivos de test.
// ❌ Antipatrón: mutación global sin posterior saneamiento
it('modifica la cabecera de autenticación global', () => {
globalThis.authToken = 'test-token';
// Si isolate: false está activo, este token afectará a los siguientes archivos
});
// ✅ Solución recomendada: preservar isolate: true en suites con efectos colaterales
// y garantizar siempre la limpieza explícita en afterEach
afterEach(() => {
delete (globalThis as Record<string, unknown>).authToken;
});Omitir la directiva define para import.meta.vitest en la configuración de Vite causará que el código de pruebas y las aserciones se empaqueten en los artefactos del cliente, engrosando innecesariamente el peso del bundle.
// ❌ Error: ausencia de define en vite.config.ts
export default defineConfig({
// Sin 'define', el bloque `if (import.meta.vitest)` sobrevivirá al tree-shaking
});
// ✅ Solución recomendada: sustitución explícita del identificador por undefined
export default defineConfig({
define: {
'import.meta.vitest': 'undefined',
},
});Almacenar estructuras HTML enteras (>200 líneas) en ficheros .snap desvirtúa el propósito del snapshot: los desarrolladores terminan actualizando con vitest -u sin verificar minuciosamente los cambios estructurales.
// ❌ Antipatrón: snapshot masivo de todo el árbol DOM de la vista
expect(container).toMatchSnapshot();
// ✅ Solución recomendada: aserciones semánticas directas sobre roles ARIA
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Panel de control');
expect(screen.getAllByRole('listitem')).toHaveLength(5);Determinados addons nativos de Node.js (por ejemplo, compilaciones antiguas de canvas o extensiones de cifrado) carecen de soporte multihilo seguro dentro de un mismo proceso y provocan fallos de segmentación (Segmentation fault).
// ❌ Error: invocar bindings nativos de C++ en worker_threads
export default defineConfig({
test: {
pool: 'threads', // Fallo por segfault en ciertos addons de C++
},
});
// ✅ Solución recomendada: configurar el pool 'forks' para tests con módulos nativos
export default defineConfig({
test: {
pool: 'forks', // Cada suite se aísla en un child_process dedicado
},
});Identificar los núcleos virtuales disponibles en el runner de CI (ej. 2 vCPUs en GitHub Actions u 8 vCPUs en servidores dedicados).
Utilizar pool: 'threads' para aplicaciones estándar de React y TypeScript. Conmutar a 'forks' únicamente ante la presencia de addons en C++.
Configurar maxThreads en (núcleos totales - 1) para evitar sobrecargar el planificador del sistema operativo y los bucles de I/O.
Activar el recolector nativo v8 sin instrumentación de Babel y almacenar en caché los artefactos entre ejecuciones del pipeline.