🇺🇸 EN
Angular
·4 min read·↗·🇺🇸 EN

Cómo probar guards funcionales en Angular con Vitest y TestBed

Portada principal del artículo: Cómo probar guards funcionales en Angular con Vitest y TestBed

⚡ ¿Usas Angular moderno (v17+ y Vitest)? Salta directo a Probar guards funcionales con Vitest para ver la suite de pruebas rápida y con soporte nativo de ESM.

En Angular moderno, los guards de rutas funcionales (CanActivateFn) reemplazaron a los guards tradicionales basados en clases (implements CanActivate).

Como los guards funcionales usan inject() en lugar de parámetros en el constructor, probarlos requiere un enfoque un poco diferente: ya no puedes simplemente instanciar una clase con new AuthGuard(mockHttp).

En esta guía práctica, te mostraré cómo escribir pruebas unitarias para guards funcionales usando TestBed.runInInjectionContext, estados de ruta simulados y Vitest.


El escenario: Un guard con feature flag

Veamos un guard funcional práctico que protege rutas según el estado de una funcionalidad de dominio. Si la funcionalidad no está disponible, redirige a /unavailable retornando un UrlTree:

// domain.guard.ts (Guard funcional moderno)
import { inject } from '@angular/core';
import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { map } from 'rxjs';
import { DomainService } from './domain.service';
 
export const domainGuard: CanActivateFn = (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot
) => {
  const router = inject(Router);
  const domainService = inject(DomainService);
 
  return domainService.isAvailable().pipe(
    map((available) => (available ? true : router.createUrlTree(['/unavailable'])))
  );
};

Por qué falla la ejecución directa (Error NG0203)

Si intentas probar esta función como si fuera una función común de JavaScript:

// ❌ FALLA: Llamar al guard directamente sin contexto de inyección
it('should test guard', () => {
  domainGuard(mockRoute, mockState); // Lanza Error: NG0203: inject() must be called from an injection context!
});

Como inject(Router) e inject(DomainService) se ejecutan dentro del cuerpo del guard, Angular necesita un contexto de inyección activo para resolver los proveedores.


Probar guards funcionales con Vitest

En Angular moderno, usamos TestBed.runInInjectionContext junto con Vitest para ejecutar el guard dentro de un entorno de inyección activo:

// domain.guard.spec.ts (Vitest + TestBed)
import { TestBed } from '@angular/core/testing';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { of } from 'rxjs';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { domainGuard } from './domain.guard';
import { DomainService } from './domain.service';
 
describe('domainGuard (Vitest)', () => {
  // 1. Simular dependencias con Vitest
  const mockDomainService = {
    isAvailable: vi.fn(),
  };
 
  const mockRouter = {
    createUrlTree: vi.fn(),
  };
 
  const mockRoute = {} as ActivatedRouteSnapshot;
  const mockState = { url: '/protected' } as RouterStateSnapshot;
 
  beforeEach(() => {
    vi.clearAllMocks();
 
    TestBed.configureTestingModule({
      providers: [
        { provide: DomainService, useValue: mockDomainService },
        { provide: Router, useValue: mockRouter },
      ],
    });
  });
 
  it('should allow access (return true) when domain is available', async () => {
    mockDomainService.isAvailable.mockReturnValue(of(true));
 
    // Ejecutar el guard dentro del contexto de inyección activo
    const result$ = TestBed.runInInjectionContext(() => domainGuard(mockRoute, mockState));
 
    result$.subscribe((result) => {
      expect(result).toBe(true);
      expect(mockRouter.createUrlTree).not.toHaveBeenCalled();
    });
  });
 
  it('should redirect to /unavailable (return UrlTree) when domain is inactive', async () => {
    const fallbackTree = new UrlTree();
    mockDomainService.isAvailable.mockReturnValue(of(false));
    mockRouter.createUrlTree.mockReturnValue(fallbackTree);
 
    const result$ = TestBed.runInInjectionContext(() => domainGuard(mockRoute, mockState));
 
    result$.subscribe((result) => {
      expect(result).toBe(fallbackTree);
      expect(mockRouter.createUrlTree).toHaveBeenCalledWith(['/unavailable']);
    });
  });
});

Pruebas en guards síncronos vs asíncronos

Los guards funcionales pueden retornar boolean, UrlTree, Promise<boolean | UrlTree> u Observable<boolean | UrlTree>.

Así puedes manejar guards síncronos de forma limpia:

// auth.guard.ts (Síncrono)
export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);
 
  return auth.isLoggedIn() ? true : router.createUrlTree(['/login']);
};
 
// auth.guard.spec.ts
it('should evaluate synchronous guard', () => {
  mockAuthService.isLoggedIn.mockReturnValue(true);
 
  const result = TestBed.runInInjectionContext(() => authGuard(mockRoute, mockState));
  expect(result).toBe(true);
});

Resumen

Probar guards funcionales es directo una vez que envuelves la llamada dentro de un contexto de inyección:

  1. Configura TestBed con las dependencias simuladas del servicio y del router usando vi.fn().
  2. Envuelve la ejecución en TestBed.runInInjectionContext(() => myGuard(route, state)).
  3. Suscríbete al observable retornado o valida el resultado síncrono (boolean o UrlTree).
Parte de la serie Angular

These are my experiences learning and facing my daily challenges working with Angular.

Ver serie completa

Preguntas Frecuentes

¿Cómo se prueba un guard funcional de Angular que utiliza la función inject()?

Los guards funcionales llaman a inject() dentro de su cuerpo. Para probarlos, envuelve la llamada del guard en TestBed.runInInjectionContext(() => myGuard(mockRoute, mockState)), configurando los servicios simulados (mocks) en TestBed.configureTestingModule.

¿Qué es TestBed.runInInjectionContext y por qué es necesario para los guards funcionales?

TestBed.runInInjectionContext ejecuta una función de callback dentro de un contexto de inyección activo de Angular, lo que permite que inject() resuelva los proveedores simulados sin lanzar el error NG0203.

¿Por qué los guards funcionales deben retornar un UrlTree en lugar de llamar a router.navigate()?

Retornar un UrlTree permite que el router de Angular gestione la cancelación y redirección de rutas de forma atómica, evitando condiciones de carrera o inconsistencias en el historial de navegación.

¿Cómo se prueban los guards funcionales con Vitest en Angular moderno?

Configura TestBed con proveedores simulados usando vi.fn() de Vitest, ejecuta el guard con TestBed.runInInjectionContext() y valida si el valor retornado es true, false o un UrlTree.

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.