Testing Functional Guards in Angular with Vitest and TestBed

⚡ Using Modern Angular (v17+ & Vitest)? Jump directly to Testing Functional Guards with Vitest to see the fast, native ESM test suite.
In modern Angular, functional route guards (CanActivateFn) replaced legacy class-based guards (implements CanActivate).
Because functional guards use inject() rather than constructor parameters, testing them requires a slightly different approach: you cannot just instantiate a class with new AuthGuard(mockHttp).
In this practical guide, I will show you how to unit test functional guards using TestBed.runInInjectionContext, mock route states, and modern Vitest.
The Scenario: A Feature Flag Guard
Let's look at a practical functional guard that protects routes based on whether a domain feature is active. If the domain is unavailable, it redirects to /unavailable by returning a UrlTree:
// domain.guard.ts (Modern Functional Guard)
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'])))
);
};Why Direct Execution Fails (NG0203 Error)
If you try testing this function like a regular JavaScript function:
// ❌ FAILS: Calling guard directly without injection context
it('should test guard', () => {
domainGuard(mockRoute, mockState); // Throws Error: NG0203: inject() must be called from an injection context!
});Because inject(Router) and inject(DomainService) are called inside the guard body, Angular needs an active Injection Context to look up the providers.
Testing Functional Guards with Vitest
In modern Angular, we use TestBed.runInInjectionContext with Vitest to execute the guard inside an active injection environment:
// 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. Mock dependencies with 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));
// Execute guard inside active injection context
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']);
});
});
});Testing Synchronous vs Asynchronous Guards
Functional guards can return boolean, UrlTree, Promise<boolean | UrlTree>, or Observable<boolean | UrlTree>.
Here is how to handle synchronous guards cleanly:
// auth.guard.ts (Synchronous)
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);
});Summary
Testing functional guards is straightforward once you wrap the call in an injection context:
- Configure
TestBedwith your mock service and router dependencies usingvi.fn(). - Wrap the execution in
TestBed.runInInjectionContext(() => myGuard(route, state)). - Subscribe to the returned observable or assert the synchronous
boolean/UrlTreeoutput.
These are my experiences learning and facing my daily challenges working with Angular.
Frequently Asked Questions
How do you test an Angular functional guard that uses the inject() function?
Functional guards call inject() inside their function body. To test them, wrap the guard call inside TestBed.runInInjectionContext(() => myGuard(mockRoute, mockState)), providing mock services in TestBed.configureTestingModule.
What is TestBed.runInInjectionContext and why is it required for functional guards?
TestBed.runInInjectionContext executes a callback function within an active Angular Injection Context, allowing inject() to resolve mock providers without throwing an NG0203 error.
Why should functional guards return a UrlTree instead of calling router.navigate()?
Returning a UrlTree allows Angular Router to handle navigation cancellation and redirects atomically without triggering race conditions or broken navigation histories.
How do you test functional guards with Vitest in modern Angular?
Configure TestBed with mock providers using Vitest's vi.fn(), run the guard via TestBed.runInInjectionContext(), and assert whether the return value is true, false, or a UrlTree.
Related Articles
How To Test Components In Angular Using TestBed
Learn how to unit test Angular components with TestBed, handle child components and service mocks, and switch from Karma/Jasmine to modern Vitest with Standalone and Signals.
How and When to Use Mocks vs Spies in Unit Testing (Vitest, Jest, and Jasmine)
Learn the practical differences between Mocks and Spies in unit testing. Explore practical examples using modern Vitest (vi.fn, vi.spyOn), Jest, and Jasmine in Angular.
Understanding and Using the inject() Function in Angular
A practical guide to Angular's inject() function: property initializers, eliminating constructor inheritance boilerplate, functional guards, and InjectionContext.
Essential Angular Interview Questions for Junior and Mid-Level Developers
A practical guide to modern Angular interview questions for junior and mid-level roles, covering Signals, Standalone APIs, Control Flow, Vitest, and Zoneless Angular.
Share this article
If you found this guide helpful, consider sharing it with your team or fellow developers.
Real Software. Real Lessons.
I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.
Join 13,800+ developers and readers.
No spam ever. Unsubscribe at any time.