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:
- Configura
TestBedcon las dependencias simuladas del servicio y del router usandovi.fn(). - Envuelve la ejecución en
TestBed.runInInjectionContext(() => myGuard(route, state)). - Suscríbete al observable retornado o valida el resultado síncrono (
booleanoUrlTree).
These are my experiences learning and facing my daily challenges working with Angular.
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
Cómo probar componentes en Angular usando TestBed
Aprende a probar componentes en Angular con TestBed, manejar componentes hijos y mocks de servicios, y migrar de Karma/Jasmine al moderno Vitest con Standalone y Signals.
Cómo y cuándo usar Mocks vs Spies en pruebas unitarias (Vitest, Jest y Jasmine)
Aprende las diferencias prácticas entre Mocks y Spies en pruebas unitarias. Revisa ejemplos prácticos con Vitest moderno (vi.fn, vi.spyOn), Jest y Jasmine en Angular.
Cómo entender y usar la función inject() en Angular
Guía práctica sobre la función inject() de Angular: inicializadores de propiedades, eliminación de código repetitivo en herencia, guards funcionales e InjectionContext.
Preguntas esenciales de entrevista en Angular para desarrolladores Junior y Mid-Level
Una guía práctica de preguntas de entrevista sobre Angular moderno para roles junior y mid-level, cubriendo Signals, Standalone APIs, Control Flow, Vitest y Zoneless Angular.
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.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.