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
}
))
}
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);
}
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()
})
})
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)
Great read! With more and more parts of Angular shifting to a more function approach, this is very helpful