Codea Bien Logo
Angular testing 2026: Guía para testear sin sufrir
Angular

Angular testing 2026: Guía para testear sin sufrir

Kevin Dávila

angulartestingvitest

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

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

  1. Escribe el test del servicio primero (o en paralelo con el desarrollo)

  2. Testea el componente con Spectator o Testing Library

  3. Agrega un test E2E para el flujo principal si es una feature crítica

  4. Corre npx vitest --coverage y revisa si hay ramas de lógica sin cubrir

  5. 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