¿Te imaginas recortar a la mitad el tiempo que tardan tus pruebas de Angular con un simple giro de tuercas que casi nadie está aplicando todavía?
Hoy vas a descubrir cómo hacerlo y por qué aplazarlo podría costarte horas de café frío y deploys retrasados.
La verdad incómoda sobre Karma
Karma ha sido el “motor” oficial durante años.
Pero en Angular 16 ya apareció en la lista de obsoletos y, desde la versión 17, vive con fecha de caducidad impresa.
Cada lanzamiento de navegador, cada reconexión y cada pestaña oculta que se abre en segundo plano es un ladrillo más en la pared que separa tu código de la luz verde del pipeline.
Por qué Jest está ganando terreno en silencio
Jest no necesita un navegador real para ejecutar tests: todo sucede en memoria, como quien hace cálculos sobre la servilleta del bar.
Los informes son legibles, coloreados y te señalan el error con una flecha casi insultante.
Además, el caché inteligente evita repetir trabajo y acelera aún más cada iteración.
La comunidad ya lo adoptó; el propio equipo de Angular lo reconoce como la opción a futuro. ¿Vas a quedarte fuera?
Tu plan exprés de 30 minutos
Actualiza dependencias
Añade el preset oficial de Angular para Jest y retira los paquetes de Karma. Con una sola orden de instalación limpias el terreno.Prepara la configuración
Reemplaza el archivo de Karma por el de Jest. Solo necesitas declarar el preset, el directorio de cobertura y los mapeos de paths que ya usas.Adapta los setup files
Si inicializabas zonas o polyfills especiales, muévelos al archivo de arranque de Jest. El formato cambia, pero el contenido es prácticamente el mismo.Ajusta los test utilities
TestBed
sigue funcionando; la diferencia es que las utilidades de Jest —spies, temporizadores falsos, snapshots— sustituyen a las de Jasmine sin dolor.Corre la primera pasada
Ejecuta tus pruebas. Lo habitual es ver una ráfaga verde y un tiempo de ejecución que parece un error de imprenta: tan rápido que querrás presumir la captura.
Lo que nadie te cuenta (pero necesitas saber)
- Cobertura instantánea: Jest muestra el porcentaje de líneas cubiertas sin plugins extra.
- Migración incremental: puedes mantener algunos archivos en Jasmine mientras terminas la transición.
- Integración continua simplificada: adiós a los launchers de navegador y a esa configuración críptica que nunca recuerdas.
¿Qué pasa si lo dejas para mañana?
Karma desaparecerá del CLI y tendrás que migrar deprisa, bajo presión y con todo el equipo mirando el reloj.
El coste no será solo técnico: cada minuto de espera en cada build se multiplicará por cada desarrollador y cada pull request.
Da el salto ahora mismo
Haz clic aquí y descarga el ebook “Unit testing con Jest y Testing Library en Angular”.
Y tendrás tu proyecto corriendo con Jest y disfrutarás de test suites que vuelan.
Recuerda el beneficio final
Migrar hoy significa ciclos de feedback rapidísimos, menos frustración y más tiempo para crear funcionalidades que importan.
No dejes que un runner obsoleto decida tu velocidad de desarrollo: adopta Jest y comprueba cómo tus pruebas dejan de ser un lastre para convertirse en tu mejor aliado.
Paso 1 – Instala lo esencial sin perder un minuto
Abre tu terminal favorita y ejecuta el gestor de paquetes para añadir Jest, jest-preset-angular y @types/jest como dependencias de desarrollo.
Con esta sola acción dejas listo el motor, el adaptador específico para Angular y las definiciones de tipos que evitarán errores de compilación más adelante.
Confirma al terminar que el archivo de bloqueos (ya sea package-lock.json
o pnpm-lock.yaml
) refleje las tres librerías; eso te garantiza entornos reproducibles en todo el equipo.
¿Listo? En menos de lo que tarda en hervir el café, ya tienes las bases instaladas y puedes pasar al siguiente ajuste de configuración.
npm install --save-dev jest jest-preset-angular @types/jest
# o si usas yarn
yarn add --dev jest jest-preset-angular @types/jest
2. Ajuste del package.json
Imagina que el corazón de tu proyecto late en el package.json
.
El próximo latido –el que activará Jest– se cocina con un simple ajuste.
Abre ese archivo y localiza el bloque scripts.
Dentro verás la orden clásica que dispara Karma; bórrala sin piedad.
En su lugar, coloca una instrucción que invoque Jest de forma directa, sin corredores intermedios ni arranques de navegador.
Con esta única sustitución, cada vez que escribas npm run test
(o el alias que uses) entrarás en la nueva era de tests turboalimentados.
Guarda el cambio, ejecuta la prueba… y comprueba cómo el cronómetro se hunde: tu suite ahora corre a la velocidad que siempre quisiste.
{
"scripts": {
"test": "npx jest"
}
}
3. Configuración de tsconfig.spec.json
¿Todavía ves el fantasma de Jasmine acechando en tu tsconfig.spec.json
?
Ese pequeño detalle frena a TypeScript y puede hacer que tus imports de Jest luzcan como errores.
Localiza la puerta de entrada
Abretsconfig.spec.json
y busca la propiedadtypes
dentro decompilerOptions
. Allí es donde “vive” la referencia a los tipos globales que entiende el compilador.Expulsa al huésped equivocado
Si la lista contiene"jasmine"
, sustitúyelo por"jest"
. Puedes mantener otras entradas útiles (por ejemplo,"node"
) si ya estaban presentes: la regla es dejar solo el conjunto de tipos que realmente necesitas.Guarda y respira
A partir de este cambio, TypeScript reconocerá todas las funciones globales de Jest (describe
,it
,expect
, etc.) sin que necesites imports extra ni comentarios de bypass. Un simplenpm run test
confirmará que las advertencias desaparecieron y que tu nueva configuración late con normalidad.
Con esa edición mínima habrás desalojado por completo los viejos tipos de Jasmine y tu proyecto quedará afinado para el nuevo corredor de pruebas.
¿Listo para el siguiente ajuste o hay algo que quieras pulir antes de continuar?
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jest", "node"] // Cambia "jasmine" por "jest"
},
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}
4. Creación de jest.config.ts
¿Preparado para darle a Jest las llaves de tu proyecto?
Crea el archivo
En la raíz, añade un nuevojest.config.ts
. Piensa en él como la consola central desde la que controlarás cada test.Importa el preset oficial
El primer paso dentro del archivo es “enchufar”jest-preset-angular
. Con eso, Jest entenderá componentes, pipes y templates como si llevara años hablando Angular.Define la configuración
-
preset: apunta a
'jest-preset-angular'
. -
testEnvironment: usa
'jsdom'
para una simulación ligera de navegador. -
setupFilesAfterEnv: referencia a un pequeño
jest.setup.ts
donde inicialicesjest-preset-angular
, fakeAsync zones o utilidades globales. -
transform: indica a Jest que los archivos
.ts
y.html
pasen por el transformador que trae el preset. -
moduleNameMapper: replica aquí los alias de paths que ya tienes en tu
tsconfig
, de modo que los imports sigan funcionando. - coverageDirectory: elige la carpeta donde quieras el informe de cobertura; así mantienes los resultados fuera de la carpeta de artefactos principales.
-
transformIgnorePatterns: excluye el código de terceros (por ejemplo,
node_modules/(?!(lodash-es|rxjs))
) si necesitas transpilar alguna librería moderna.
-
Guarda y prueba
Con solo ese puñado de líneas, Jest sabe exactamente cómo arrancar tus specs de Angular. Lanza
npm run test
y observa cómo se disparan los resultados: rápido, limpio y con un informe de cobertura listo para presumir.
Sin tocar una sola línea de tu código de producción, habrás levantado la nueva central de mando para tus pruebas unitarias. Tu stack ya funciona con Jest; lo siguiente es disfrutar de ciclos de feedback que casi no dan tiempo ni a parpadear.
import type { Config } from 'jest';
const config: Config = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
moduleDirectories: ['node_modules', '<rootDir>'],
testMatch: ['**/+(*.)+(spec).+(ts)'],
};
export default config;
5. Creación de setup-jest.ts
¿Sabías que un único archivo puede convocar todo el poder de Jest y al mismo tiempo convencer a Angular de que nada ha cambiado? Ese archivo se llama setup-jest.ts
, y su misión es dejar listo el escenario antes de que se ejecute la primera prueba.
Por qué importa
Jest necesita que le cuentes cómo funcionan las zonas de Angular, cómo compilar plantillas y en qué momento «pinchar» el TestBed. Sin esa preparación, tus components quedarían mudos y tus pipes perderían la voz.
Lo que debe ocurrir dentro
Encendido del preset
El archivo arranca cargando la utilería que traduce internamente los decoradores, las plantillas y las dependencias que Angular usa de forma nativa.Parches y polyfills opcionales
Si tu app depende de APIs del navegador que JSDOM no expone por defecto—comomatchMedia
o ciertas animaciones—, aquí añades los parches mínimos para que Jest no tropiece.Extensiones a las expectativas
Puedes incorporar ayudas de aserción adicionales—por ejemplo, comparadores de fechas o números aproximados—sin repetir imports en cada archivo de prueba.Configuración global de tiempo
Ajustar un timeout genérico evita que las pruebas se queden colgadas por operaciones lentas, manteniendo el reporte limpio y controlado.
Con esa coreografía, cada spec encuentra un Angular completamente operativo en milésimas de segundo, sin demoras ni advertencias extrañas. Guarda el archivo junto al resto de tu código y lánzate a ejecutar npm run test
: verás cómo todo fluye con la seguridad de saber que tu entorno está perfectamente preparado antes de que el cronómetro arranque.
import 'jest-preset-angular/setup-jest';
import '@angular/localize/init'; // Necesario para Angular >= 9
// Workaround para ReferenceError: TextEncoder is not defined en Jest
import { TextEncoder } from 'node:util';
Object.defineProperty(window, 'TextEncoder', {
writable: true,
value: TextEncoder,
});
6. Deja atrás el lastre de Karma y Jasmine
Ahora que tu suite vuela con Jest, mantener las viejas dependencias es como llevar un ancla colgada al tobillo. Deshazte de Karma, Jasmine y cualquier launcher o reporter relacionado: liberarás espacio, acelerarás las instalaciones de CI y evitarás conflictos de versiones en el futuro.
Depuración del
package.json
Despídete de cada librería que empiece por karma- o jasmine-. Al retirarlas, tu árbol de dependencias se encoge y las descargas en cada pipeline se reducen a la mitad.Limpieza de archivos de configuración
Borrakarma.conf.js
,test.ts
(si lo tienes) y cualquier script de npm que invoque Karma. Si tu CI aún abre un navegador para los tests, elimínalo también: ese paso dejó de tener sentido.Revisión de scripts y tasks
Comprueba que no queden comandos heredados en la carpeta de herramientas, ni pasos de build que referencien al viejo runner. Una búsqueda rápida por el repositorio te dará la certeza.Actualización del cache de CI
Sin Karma ni Chrome Headless en la ecuación, tu contenedor construirá menos capas y completará los tests antes de que acabes el primer sorbo de café.Confirmación final
Ejecuta la suite con Jest en local y en la pipeline. Verás cómo la lista de paquetes descargados se acorta y los tiempos de ejecución marcan nuevos mínimos históricos.
Con esta purga, tu proyecto queda más ligero, tu presupuesto de CI respira y tu equipo trabaja sobre un stack coherente y enfocado en el futuro. Sigue avanzando; cada archivo antiguo que eliminas es un segundo menos de espera en tu próximo deploy.
npm uninstall karma karma-chrome-launcher karma-jasmine karma-coverage karma-jasmine-html-reporter @types/jasmine @types/jasminewd2 jasmine-core jasmine-spec-reporter
Ejemplo mínimo: Conversión de una prueba tradicional a Jest con Testing Library
Veamos un ejemplo sencillo de cómo se vería una prueba antes y después de la migración.
Componente CounterComponent
(ejemplo):
// counter.component.ts
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span data-testid="count">{{ count() }}</span>
<button (click)="increment()">+</button>
`,
standalone: true,
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update((value) => value + 1);
}
decrement() {
this.count.update((value) => value - 1);
}
}
Prueba tradicional (Karma/Jasmine con TestBed
):
// counter.component.spec.ts (Antes)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
let compiled: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent],
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
compiled = fixture.nativeElement;
fixture.detectChanges();
});
it('Haves an initial count of 0', () => {
expect(component.count()).toBe(0);
});
it('Renders the initial count of 0 in the span', () => {
const countSpan = compiled.querySelector('[data-testid="count"]');
expect(countSpan?.textContent).toBe('0');
});
it('Increment the count signal when increment() is called', () => {
expect(component.count()).toBe(0);
component.increment();
expect(component.count()).toBe(1);
});
it('Decrement the count signal when decrement() is called', () => {
expect(component.count()).toBe(0);
component.decrement();
expect(component.count()).toBe(-1);
});
it('Increment the count in the template when the "+" button is clicked', () => {
const incrementButton = compiled.querySelectorAll('button')[1];
const countSpan = compiled.querySelector('[data-testid="count"]');
incrementButton.click();
fixture.detectChanges();
expect(component.count()).toBe(1);
expect(countSpan?.textContent).toBe('1');
});
it('Decrement the count in the template when the "-" button is clicked', () => {
const decrementButton = compiled.querySelector('button');
const countSpan = compiled.querySelector('[data-testid="count"]');
expect(countSpan?.textContent).toBe('0');
decrementButton?.click();
fixture.detectChanges();
expect(component.count()).toBe(-1);
expect(countSpan?.textContent).toBe('-1');
});
});
Prueba migrada (Jest):
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent (con Jest) - Versión Mejorada', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
let compiled: HTMLElement;
const getElement = (selector: string): HTMLElement | null => compiled.querySelector(selector);
const getButton = (text: string): HTMLButtonElement | null => {
return Array.from(compiled.querySelectorAll('button')).find((btn) => btn.textContent === text) || null;
};
const getCountSpan = (): HTMLElement | null => getElement('[data-testid="count"]');
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent],
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
compiled = fixture.nativeElement;
fixture.detectChanges();
});
describe('Initial State and Rendering', () => {
it('Initialize with a count of 0', () => {
expect(component.count()).toBe(0);
});
it('Renders the initial count "0" inside the span', () => {
const countSpan = getCountSpan();
expect(countSpan).not.toBeNull();
expect(countSpan?.textContent).toBe('0');
});
it('Renders an increment and a decrement button', () => {
const incrementButton = getButton('+');
const decrementButton = getButton('-');
expect(incrementButton).not.toBeNull();
expect(decrementButton).not.toBeNull();
});
});
describe('Business Logic', () => {
it('Update the count signal from 0 to 1', () => {
component.increment();
expect(component.count()).toBe(1);
});
it('Update the count signal from 0 to -1', () => {
component.decrement();
expect(component.count()).toBe(-1);
});
it('Handles multiple operations correctly (e.g., increment twice, decrement once)', () => {
component.increment();
component.increment();
component.decrement();
expect(component.count()).toBe(1);
});
});
describe('User Interaction', () => {
it('Update the displayed count to "1" when the "+" button is clicked', () => {
const incrementButton = getButton('+');
incrementButton?.click();
fixture.detectChanges();
const countSpan = getCountSpan();
expect(component.count()).toBe(1);
expect(countSpan?.textContent).toBe('1');
});
it('Update the displayed count to "-1" when the "-" button is clicked', () => {
const decrementButton = getButton('-');
decrementButton?.click();
fixture.detectChanges();
const countSpan = getCountSpan();
expect(component.count()).toBe(-1);
expect(countSpan?.textContent).toBe('-1');
});
it('Displays "0" after clicking "+" and then "-"', () => {
const incrementButton = getButton('+');
const decrementButton = getButton('-');
incrementButton?.click();
fixture.detectChanges();
decrementButton?.click();
fixture.detectChanges();
const countSpan = getCountSpan();
expect(component.count()).toBe(0);
expect(countSpan?.textContent).toBe('0');
});
});
});
¿Lo mejor de todo? Ahora tu archivo de pruebas no solo “pasa”, sino que respira mantenimiento futuro.
Por qué tus ajustes son oro puro
Selectores resilientes
Buscar por texto (+ / −) anula la fragilidad del índice numérico. Mañana podrás insertar un botón “reset” en cualquier parte y el test ni se despeinará.Describe anidados que cuentan una historia
Al separar Estado Inicial, Lógica y Interacción, cualquiera localiza la intención en segundos. Leer el archivo deja de ser un safari deit()
sueltos y pasa a ser un mapa bien señalizado.Helpers DRY
Centralizar la obtención de elementos reduce el “ruido” en cada spec y concentra el cambio en un único punto. Si algún día migras dedata-testid
a atributos ARIA, actualizarás solo esas funciones.Cobertura real de flujos
Verificar la presencia de ambos botones y la secuencia + → − garantiza que tu contador no falla ante combinaciones encadenadas. Es un test de comportamiento, no una foto estática.
Cómo exprimir aún más tu nueva base
Añade tests de accesibilidad
Usajest-axe
para asegurarte de que los cambios visuales no rompen reglas WCAG. Mantendrás al equipo en verde respecto a inclusión sin esfuerzo manual.Integra snapshots selectivos
Útiles para componentes con mucho markup. Hazlo solo en piezas poco volátiles; así evitarás falsas alarmas en cada refactor visual menor.Mide la cobertura de ramas
ActivacoverageReporters: ['text', 'lcov']
y revisa los puntos ciegos. Verás con claridad qué caminos lógicos no pisan todavía tus pruebas.Optimiza los fake timers
Para lógica basada ensetTimeout
odebounce
, los temporizadores controlados de Jest aceleran las specs y evitan esperas innecesarias.Revisa tu pipeline de CI
Ahora que Karma salió de escena, ajusta el caché de dependencias y elimina pasos de navegador. Ganas minutos en cada ejecución automática.
Has sentado un cimiento robusto: pruebas legibles, resistentes y fáciles de ampliar. A partir de aquí, cualquier nueva funcionalidad se incorporará con confianza y sin temer al efecto dominó.
Bonus: Jest y Angular Testing Library
¿Listo para ver cómo el mismo test respira libertad sin fixture
, sin detectChanges()
y sin ese ruido que encoge la vista?
Observa esta versión con Angular Testing Library: minimalista, robusta y pensada para el futuro.
Prueba migrada (Jest y Angular Testing Library):
// counter.component.spec.ts (Después)
import { render, screen, fireEvent } from '@testing-library/angular';
import { CounterComponent } from './counter.component';
import '@testing-library/jest-dom'; // Para matchers extendidos como.toBeInTheDocument()
describe('CounterComponent (Jest/Testing Library)', () => {
// Helper centralizado: un solo punto de cambio
const setup = async () => {
await render(CounterComponent); // crea componente + fixture por ti
const getButton = (label: string) => screen.getByRole('button', { name: label });
return {
getCount: () => screen.getByTestId('count'),
getButton,
};
};
it('Create and display initial count of 0', async () => {
const { getCount } = await setup();
expect(getCount()).toHaveTextContent('0');
});
it('Increment count when + button is clicked', async () => {
const { getButton, getCount } = await setup();
fireEvent.click(getButton('+'));
expect(getCount()).toHaveTextContent('1');
});
it('Decrement count when - button is clicked', async () => {
const { getButton, getCount } = await setup();
fireEvent.click(getButton('-'));
expect(getCount()).toHaveTextContent('-1');
});
});
-
Sin fixtures ni
TestBed.compileComponents()
.render()
hace la magia tras bambalinas. -
Selectores accesibles (
getByRole
): resistentes a cambios de orden o estilo. -
Flujos síncronos por defecto: cuando algo sea realmente asíncrono, usa
await waitFor(() => …)
y listo.
¿Te gustó la sensación de limpieza? En mi libro sobre testing moderno en Angular, profundizo en querying accesible, patrones de state observers y prácticas de rendimiento que hacen que tus tests vuelen.
¿Prefieres aprender mirando? Aquí tienes tu atajo visual
Sé que la instalación de Angular Testing Library merece un tutorial aparte; lo verás detallado en el próximo artículo.
Mientras tanto, si eres de los que absorben conceptos viendo cada clic en tiempo real, he preparado un vídeo exprés donde compruebas, paso a paso, cómo escribir un unit test de un input required
con Jest.
▶️ *Ver el vídeo en YouTube *
Beneficios medibles: Tiempo de ejecución y reporte de cobertura
En pocas líneas:
Velocidad a otro nivel
Jest ejecuta tests en paralelo y en JSDOM; suites que tardaban 15 min con Karma pueden bajar a 1-2 min.Cobertura con un flag
jest --coverage
genera informes detallados sin plugins extra, así ves al instante qué ramas faltan por probar.Mocking integrado y simple
Conjest.fn()
ojest.mock()
aísla servicios y APIs globales sin configurar librerías adicionales.Snapshot testing
Captura la salida del componente y detecta al momento cualquier cambio inesperado en la UI.
En conjunto, obtienes ciclos de feedback mucho más rápidos, builds de CI más baratas y una confianza mayor en cada refactor.
Conclusión
Dar el salto de Karma/Jasmine a Jest es pasar de un motor con años de servicio a uno pensado para la velocidad y la claridad de hoy. En minutos tendrás:
- Pruebas que se ejecutan en paralelo y terminan antes de que apures el café.
- Cobertura de código con un simple
--coverage
, sin plugins ni rodeos. - Mocks y snapshots listos para blindar cada refactor.
Cada segundo ganado en el test runner vuelve a tu desarrollo: menos espera, más funciones nuevas, mayor confianza.
¿Quieres ver cómo aplicar Jest a casos reales y tener ejemplos de producción listos para copiar y pegar? Descúbrelo en nuestro eBook y lleva tus pruebas al siguiente nivel. Haz clic aquí y conviértete en el desarrollador que rompe la barra de progreso antes de que empiece a cargar.
Top comments (0)