---
title: "Testing Functional Guards in Angular with Vitest and TestBed"
date: "2023-04-16"
slug: "testing-functional-guards-in-angular-15-a-step-by-step-guide"
author: "Dany Paredes"
canonical: "https://danywalls.com/testing-functional-guards-in-angular-15-a-step-by-step-guide"
description: "Learn how to unit test Angular functional route guards using TestBed.runInInjectionContext, mock services, and modern Vitest test suites."
---


> ⚡ **Using Modern Angular (v17+ & Vitest)?** Jump directly to [Testing Functional Guards with Vitest](#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`:

```typescript
// 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:

```typescript
// ❌ 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:

```typescript
// 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:

```typescript
// 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:
1. Configure `TestBed` with your mock service and router dependencies using `vi.fn()`.
2. Wrap the execution in **`TestBed.runInInjectionContext(() => myGuard(route, state))`**.
3. Subscribe to the returned observable or assert the synchronous `boolean` / `UrlTree` output.

