DEV Community

Cover image for How To Test Functional Guards in Angular 15
Dany Paredes
Dany Paredes

Posted on

How To Test Functional Guards in Angular 15

Since Angular 14, we can convert our class guards to functional and also combine them with inject making it so easy to write guards Angular applications.

When we move from class to functional, the constructor disappears, and the dependencies come from inject, but how much is the testing impacted?

Functional Guard

In a prior blog post about functional guards, we explored the shift from class-based to functional guards in Angular applications.

We have a functional guard, domainGuard\; it uses the inject function to get two dependencies: the router and the Domain Service.

import {inject} from '@angular/core';
import {Router} from '@angular/router';
import {tap} from 'rxjs';
import {DomainService} from '../domain.service';

export const domainGuard = () => {
    const router = inject(Router);
    const service = inject(DomainService)
    return service.isAvailable().pipe(
    tap((value) => {
      return !value ? router.navigate(['/no-available']) : true
    }
  ))
}
Enter fullscreen mode Exit fullscreen mode

We'll be utilizing TestBed along with a custom function to ensure thorough test coverage. Let's dive in!

Using Testbed

Using Testbed simplifies the process of configuring tests, but remember that we use the inject function to supply our dependencies, and the inject function can be found within the function body only.

But In Angular 15, the runInInjectionContext feature enables us to supply our dependencies previously provided through the inject function.

Read more https://angular.io/api/core/testing/TestBed#runInInjectionContext

Instead of using BeforeEach, I've developed a custom setup function that generates an instance of our guard with TestBed.

The domainGuard relies on two dependencies: router and domainService.

  • Rather than importing the RouterTestingModule, I'll mock the Router using createSpyObject\.

  • The setup function takes a domainServiceMock parameter, allowing me to tailor the domainService behavior for each test.

  • Retrieve the domainGuard instance by utilizing TestBed.runInInjectionContext\.

The final code looks like this:

const mockRouter = jasmine.createSpyObj<Router>(['navigate'])
  mockRouter.navigate.and.returnValue(lastValueFrom(of(true)))

  const setup = (domainServiceMock: unknown) => {
    TestBed.configureTestingModule({
      providers: [
        domainGuard,
        { provide: DomainService, useValue: domainServiceMock},
        { provide: Router, useValue: mockRouter}
      ]
    });

    return TestBed.runInInjectionContext(domainGuard);
  }
Enter fullscreen mode Exit fullscreen mode

Write Tests

We'll be writing two tests, and for each, we'll invoke the setup function to obtain the guard instance. This allows us to tailor the domainService according to our test requirements.

The final code looks like this:

  it('should allow to continue', () => {

    const mockDomainService : unknown = {
      isAvailable: () => of(true)
    }
    const guard = setup(mockDomainService);
    guard.subscribe((p: unknown) => {
      expect(p).toBe(true)
    })
  })

  it('should redirect to /no-available path', () => {

    const mockDomainService: unknown = {
      isAvailable: () => of(false)
    }

    const guard = setup(mockDomainService);
    guard.subscribe((p: unknown) => {
      expect(p).toBe(false)
      expect(mockRouter.navigate).toHaveBeenCalled()
    })
  })
Enter fullscreen mode Exit fullscreen mode

Done! We are testing our functional guard easily!

Final

Transforming our class guard into a functional format is remarkably straightforward, and the necessary modifications to our tests are minimal. The key to achieving this lies in the clever utilization of TestBed.runInInjectionContext in conjunction with Testbed, allowing for a seamless transition and enhanced functionality.

If you have questions, concerns, or thoughts, please comment below. For a deeper technical dive, check out the source code on GitHub.

Photo by Julia Koblitz on Unsplash

Top comments (1)

Collapse
 
pbouillon profile image
Pierre Bouillon

Great read! With more and more parts of Angular shifting to a more function approach, this is very helpful