Angular testing 2026: Guía para testear sin sufrir
Kevin Dávila
Testear en Angular tiene fama de ser complicado. Y durante varios años, lo fue.
Karma, Webpack, configuraciones interminables, tests que tardaban una eternidad en correr. Mucha gente simplemente dejaba de hacerlo. (Me incluyo u.u)
Eso cambió. En 2026, el ecosistema de testing en Angular es más limpio, más rápido y más accesible que nunca.
Hoy el panorama completo: qué herramientas existen, cuándo usar cada una, y cómo escribir tests que realmente aporten valor.
El panorama actual
Antes de entrar en código, conviene entender dónde estamos parados.
Karma está muerto. El Angular team lo deprecó oficialmente. Si tu proyecto todavía lo usa, es momento de migrar.
Vitest es el nuevo estándar. Angular migró a Vitest como runner de tests por defecto. Es rápido, tiene excelente soporte para TypeScript y funciona sin configuración elaborada.
Jest sigue siendo una opción válida, especialmente en proyectos que ya lo tienen integrado o en monorepos con Nx. (Pequeña historia, justo cuando estabilizamos bien jest en mi proyecto, el angular team migró a vitest xd)
Playwright domina el E2E. Cypress sigue siendo popular, pero Playwright se convirtió en la opción preferida por velocidad, confiabilidad y soporte de browsers.
Setup básico con Vitest
Angular CLI genera la configuración por defecto. Pero si estás migrando desde Karma:
ng generate config vitest
Esto genera el archivo de configuración:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig({
plugins: [angular()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
include: ['src/**/*.spec.ts'],
coverage: {
reporter: ['text', 'html'],
exclude: ['node_modules/', 'src/test-setup.ts'],
},
},
});
// src/test-setup.ts
import '@angular/compiler';
import { setupZonelessTestEnv } from 'zone.js/testing';
setupZonelessTestEnv();
Con esto, corres tus tests con:
npx vitest
npx vitest --coverage
npx vitest --ui # interfaz visual en el browser
Source: https://vitest.dev/guide/
TestBed: la base de todo
TestBed es el entorno de pruebas de Angular. Te permite crear componentes, inyectar servicios y simular el árbol de dependencias en un contexto controlado.
// contador.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { ContadorComponent } from './contador.component';
describe('ContadorComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContadorComponent],
}).compileComponents();
});
it('debería iniciar en 0', () => {
const fixture = TestBed.createComponent(ContadorComponent);
fixture.detectChanges();
const el = fixture.nativeElement as HTMLElement;
expect(el.querySelector('[data-testid="contador"]')?.textContent).toBe('0');
});
it('debería incrementar al hacer click', () => {
const fixture = TestBed.createComponent(ContadorComponent);
fixture.detectChanges();
const boton = fixture.nativeElement.querySelector('button') as HTMLButtonElement;
boton.click();
fixture.detectChanges();
const contador = fixture.nativeElement.querySelector('[data-testid="contador"]');
expect(contador?.textContent).toBe('1');
});
});
Funciona. Pero tiene muuuucho boilerplate. Para un componente simple necesitas varios bloques de setup. Cuando tienes dependencias, el setup se complica más.
Ahí es donde entran las herramientas externas.
Spectator: menos boilerplate, más claridad
Spectator es una librería que envuelve TestBed con una API mucho más simple. Reduce drásticamente la cantidad de código que necesitas para configurar un test.
npm install @ngneat/spectator --save-dev
El mismo test de arriba, con Spectator:
// contador.component.spec.ts
import { createComponentFactory, Spectator } from '@ngneat/spectator';
import { ContadorComponent } from './contador.component';
describe('ContadorComponent', () => {
let spectator: Spectator<ContadorComponent>;
const createComponent = createComponentFactory(ContadorComponent);
beforeEach(() => {
spectator = createComponent();
});
it('debería iniciar en 0', () => {
expect(spectator.query('[data-testid="contador"]')).toHaveText('0');
});
it('debería incrementar al hacer click', () => {
spectator.click('button');
expect(spectator.query('[data-testid="contador"]')).toHaveText('1');
});
});
Mucho más limpio. Spectator se encarga del ciclo de compileComponents y detectChanges. La API de spectator.query, spectator.click, spectator.typeInElement simplifica la interacción con el DOM.
Testeando servicios con Spectator
// auth.service.spec.ts
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let spectator: SpectatorService<AuthService>;
let httpMock: HttpTestingController;
const createService = createServiceFactory({
service: AuthService,
imports: [HttpClientTestingModule],
});
beforeEach(() => {
spectator = createService();
httpMock = spectator.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('debería hacer POST a /auth/login', () => {
spectator.service.login('user@test.com', 'password123').subscribe();
const req = httpMock.expectOne('/auth/login');
expect(req.request.method).toBe('POST');
req.flush({ token: 'abc123' });
});
});
Mockeando dependencias con Spectator
// dashboard.component.spec.ts
import { createComponentFactory, mockProvider } from '@ngneat/spectator';
import { DashboardComponent } from './dashboard.component';
import { UsuarioService } from '../services/usuario.service';
import { of } from 'rxjs';
describe('DashboardComponent', () => {
const createComponent = createComponentFactory({
component: DashboardComponent,
providers: [
mockProvider(UsuarioService, {
getUsuarioActual: () => of({ nombre: 'Kevin', rol: 'admin' }),
}),
],
});
it('debería mostrar el nombre del usuario', () => {
const spectator = createComponent();
expect(spectator.query('[data-testid="nombre-usuario"]')).toHaveText('Kevin');
});
});
mockProvider crea un mock automático del servicio y te permite sobreescribir solo los métodos que necesitas para ese test específico.
Angular Testing Library: testea como lo haría el usuario
@testing-library/angular aplica una filosofía diferente: no testees la implementación, testea el comportamiento desde la perspectiva del usuario.
npm install @testing-library/angular --save-dev
Lo fundamental de Testing Library es encontrar elementos como los encontraría un usuario, no buscando clases CSS o estructura interna.
// login.component.spec.ts
import { render, screen, fireEvent, waitFor } from '@testing-library/angular';
import { userEvent } from '@testing-library/user-event';
import { LoginComponent } from './login.component';
import { AuthService } from '../services/auth.service';
describe('LoginComponent', () => {
it('debería llamar a login con las credenciales correctas', async () => {
const mockAuthService = {
login: vi.fn().mockReturnValue(of({ token: 'abc' })),
};
await render(LoginComponent, {
providers: [{ provide: AuthService, useValue: mockAuthService }],
});
const user = userEvent.setup();
await user.type(screen.getByLabelText('Correo electrónico'), 'kevin@test.com');
await user.type(screen.getByLabelText('Contraseña'), 'mipassword');
await user.click(screen.getByRole('button', { name: 'Iniciar sesión' }));
expect(mockAuthService.login).toHaveBeenCalledWith('kevin@test.com', 'mipassword');
});
it('debería mostrar error si el campo está vacío', async () => {
await render(LoginComponent, {
providers: [{ provide: AuthService, useValue: { login: vi.fn() } }],
});
await userEvent.setup().click(screen.getByRole('button', { name: 'Iniciar sesión' }));
expect(screen.getByRole('alert')).toHaveTextContent('El correo es requerido');
});
});
Observa los queries: getByLabelText, getByRole. Estos buscan elementos por cómo los percibe un usuario, no por clases o IDs internos. Si el test pasa, el componente es accesible y funcional al mismo tiempo.
Testeando Signals
Angular usa signals para el estado reactivo. Son síncronos y sencillos de testear.
// carrito.service.ts
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CarritoService {
private items = signal<Producto[]>([]);
readonly total = computed(() =>
this.items().reduce((acc, item) => acc + item.precio, 0)
);
agregar(producto: Producto) {
this.items.update(items => [...items, producto]);
}
vaciar() {
this.items.set([]);
}
cantidad() {
return this.items().length;
}
}
// carrito.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { CarritoService } from './carrito.service';
describe('CarritoService', () => {
let service: CarritoService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CarritoService);
});
it('debería iniciar vacío', () => {
expect(service.cantidad()).toBe(0);
expect(service.total()).toBe(0);
});
it('debería agregar productos y calcular el total', () => {
service.agregar({ id: 1, nombre: 'Teclado', precio: 800 });
service.agregar({ id: 2, nombre: 'Mouse', precio: 400 });
expect(service.cantidad()).toBe(2);
expect(service.total()).toBe(1200);
});
it('debería vaciarse correctamente', () => {
service.agregar({ id: 1, nombre: 'Teclado', precio: 800 });
service.vaciar();
expect(service.cantidad()).toBe(0);
});
});
Los signals son síncronos. No necesitas fakeAsync, tick ni detectChanges para leer su valor. Eso hace los tests notablemente más simples que con observables.
Testeando componentes con signals como inputs
Con withComponentInputBinding(), los inputs de los componentes pueden recibir valores directamente:
// producto-card.component.spec.ts
import { createComponentFactory } from '@ngneat/spectator';
import { ProductoCardComponent } from './producto-card.component';
describe('ProductoCardComponent', () => {
const createComponent = createComponentFactory({
component: ProductoCardComponent,
});
it('debería mostrar el nombre y precio del producto', () => {
const spectator = createComponent({
props: {
nombre: 'Laptop Pro',
precio: 25000,
},
});
expect(spectator.query('[data-testid="nombre"]')).toHaveText('Laptop Pro');
expect(spectator.query('[data-testid="precio"]')).toHaveText('$25,000');
});
});
Testeando Guards
Los guards son funciones. Se testean como funciones.
// auth.guard.spec.ts
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { authGuard } from './auth.guard';
import { AuthService } from '../services/auth.service';
describe('authGuard', () => {
let router: Router;
const mockAuthService = {
isLoggedIn: vi.fn(),
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [
{ provide: AuthService, useValue: mockAuthService },
],
});
router = TestBed.inject(Router);
});
it('debería permitir acceso si el usuario está autenticado', () => {
mockAuthService.isLoggedIn.mockReturnValue(true);
const result = TestBed.runInInjectionContext(() => authGuard({} as any, {} as any));
expect(result).toBe(true);
});
it('debería redirigir a /login si no está autenticado', () => {
mockAuthService.isLoggedIn.mockReturnValue(false);
const result = TestBed.runInInjectionContext(() => authGuard({} as any, {} as any));
expect(result).toEqual(router.createUrlTree(['/login']));
});
});
TestBed.runInInjectionContext es clave para testear funciones que usan inject() internamente.
Testeando Resolvers
// producto.resolver.spec.ts
import { TestBed } from '@angular/core/testing';
import { ActivatedRouteSnapshot } from '@angular/router';
import { productoResolver } from './producto.resolver';
import { ProductoService } from '../services/producto.service';
import { of } from 'rxjs';
describe('productoResolver', () => {
const mockProductoService = {
getById: vi.fn(),
};
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: ProductoService, useValue: mockProductoService },
],
});
});
it('debería retornar el producto por id', (done) => {
const productoMock = { id: '42', nombre: 'Monitor 4K', precio: 9000 };
mockProductoService.getById.mockReturnValue(of(productoMock));
const route = {
paramMap: { get: () => '42' },
} as unknown as ActivatedRouteSnapshot;
const result$ = TestBed.runInInjectionContext(() =>
productoResolver(route, {} as any)
);
(result$ as Observable<any>).subscribe(producto => {
expect(producto).toEqual(productoMock);
expect(mockProductoService.getById).toHaveBeenCalledWith('42');
done();
});
});
});
E2E con Playwright
Los tests unitarios validan piezas. Los tests E2E validan flujos completos. Para eso, Playwright.
npm install @playwright/test --save-dev
npx playwright install
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Flujo de login', () => {
test('debería iniciar sesión con credenciales válidas', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Correo electrónico').fill('kevin@test.com');
await page.getByLabel('Contraseña').fill('password123');
await page.getByRole('button', { name: 'Iniciar sesión' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Bienvenido, Kevin')).toBeVisible();
});
test('debería mostrar error con credenciales inválidas', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Correo electrónico').fill('nadie@test.com');
await page.getByLabel('Contraseña').fill('wrongpass');
await page.getByRole('button', { name: 'Iniciar sesión' }).click();
await expect(page.getByRole('alert')).toContainText('Credenciales incorrectas');
await expect(page).toHaveURL('/login');
});
});
La API de Playwright es intuitiva. getByLabel, getByRole, getByText buscan elementos como los buscaría un usuario. Playwright también tiene modo de depuración visual y puede generar tests grabando las interacciones.
Buenas prácticas
Usa data-testid para seleccionar elementos en tests unitarios. Las clases CSS cambian. Los textos cambian. Un data-testid="boton-login" es un contrato explícito entre el componente y el test.
<!-- Bien -->
<button data-testid="boton-submit">Guardar</button>
<!-- Evitar en tests -->
<button class="btn btn-primary">Guardar</button>
En E2E, usa roles y labels en lugar de data-testid. Playwright debe encontrar elementos como un usuario. Si usas roles, además estás validando que el componente es accesible.
Un test, una cosa. Si un test verifica que el botón está deshabilitado Y que muestra un mensaje Y que llama al servicio — es demasiado. Divide en tres tests.
Testea el comportamiento, no la implementación. No testees que this.usuario = data. Testea que después de cargar, el nombre del usuario aparece en pantalla.
Aísla bien los mocks. Usa vi.clearAllMocks() o vi.resetAllMocks() en el afterEach para evitar que el estado de un test afecte al siguiente.
afterEach(() => {
vi.clearAllMocks();
});
Corre los tests en modo watch durante desarrollo. npx vitest --watch recorre solo los archivos que cambiaron. El feedback es inmediato.
Mide cobertura, pero no la idolatres. 80% de cobertura no significa que tu app funciona. Significa que el 80% del código se ejecutó en algún test. La calidad del test importa más que el número.
Lo que NO debes hacer
No testees los detalles internos del componente. Si cambias la variable privada de isLoading a cargando, el test no debería romperse. Si se rompe, estás testeando implementación, no comportamiento.
No uses fixture.debugElement.query(By.css('.mi-clase')) para todo. Es frágil. Prefiere data-testid o queries semánticos.
No ignores los errores de consola en los tests. Angular imprime warnings cuando algo no está bien configurado. Muchos desarrolladores los ignoran. Son señales reales.
// Convierte los console.error en fallos para no ignorarlos
beforeEach(() => {
vi.spyOn(console, 'error').mockImplementation(() => {
throw new Error('console.error fue llamado en este test');
});
});
No escribas tests solo para subir el coverage. Un test que no puede fallar es inútil. Si el test siempre pasa sin importar lo que haga el componente, bórralo.
No hagas tests E2E de todo. Los tests E2E son lentos y frágiles. Úsalos para los flujos críticos: login, checkout, flujo de registro. El resto, cúbrelo con tests unitarios.
Cuándo usar cada herramienta
Herramienta | Para qué |
|---|---|
Vitest + TestBed | Lógica de servicios, pipes, guards, resolvers |
Spectator | Componentes con muchas dependencias, reduce boilerplate |
Angular Testing Library | Componentes de UI que el usuario interactúa directamente |
Playwright | Flujos críticos de usuario end-to-end |
jest-axe / axe-core | Validación automática de accesibilidad |
Flujo recomendado para una feature nueva
Escribe el test del servicio primero (o en paralelo con el desarrollo)
Testea el componente con Spectator o Testing Library
Agrega un test E2E para el flujo principal si es una feature crítica
Corre npx vitest --coverage y revisa si hay ramas de lógica sin cubrir
Revisa los tests en el PR antes de hacer merge
Conclusión
Testear en Angular en 2026 ya no es una pesadilla de configuración. Con Vitest como runner, Spectator para reducir boilerplate, Testing Library para validar comportamiento real y Playwright para E2E, tienes todo lo que necesitas para un pipeline de tests sólido.
La mentalidad importa más que las herramientas. Testear no es un requisito burocrático. Es la forma más confiable de saber que lo que construiste funciona, y seguirá funcionando cuando alguien lo toque tres meses después.
Keywords
Angular Testing 2026
Vitest Angular
Spectator Angular
Angular Testing Library
Playwright Angular E2E