Testing for functional guards
Description
There does not seem to be a proper way to test functional guard with spectator as yet.
It seems like they need to be tested using angular runInInjectionContext method.
https://netbasal.com/getting-to-know-the-runincontext-api-in-angular-f8996d7e00da
It would be nice to have first-class support for functional guards.
Proposed solution
Create a new API to test functional guards that can use spectators injector.
export const functionalGuard = (() => {
const someService = inject(SomeService);
return someService.canDoStuff();
}) satisfies CanActivateFn;
describe('functionalGuard', () => {
let spectator: Spectator<theFunctionalGuard>;
const createFunctionalGuard = createFunctionalGuardFactory({
guard: theFunctionalGuard,
});
beforeEach(() => {
spectator = createFunctionalGuard();
});
it('should test the guard', () => {
spectator.runInInjectionContext((guard) => {
// set some inputs for the guard, mock values
expect(guard).toReturnWith(true);
});
});
});
Alternatives considered
More so a work-around than an alternative, creating an empty component and "stealing" its injector.
@Component({
selector: 'app-empty-component',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
class EmptyComponent {}
describe('functionalGuard', () => {
let spectator: Spectator<EmptyComponent>;
const createComponent = createComponentFactory(EmptyComponent);
beforeEach(() => {
spectator = createComponent();
});
it('should test the guard', () => {
runInInjectionContext(spectator.inject(EnvironmentInjector), () => {
// set some inputs for the guard, mock values
expect(functionalGuard).toReturnWith(true);
});
});
});
Do you want to create a pull request?
I'd be open to trying to create a pull request if we can settle on a nice API.
You are welcome to play with some ideas you have and see what's the best fit and open a PR.
I've been working on this on-and-off and it turns out to be quite a bit more annoying than I anticipated.
The first step seems to be using TestBed.runInInjectionContext, this accepts a callback which will enable the use of the inject method.
Exposing it via the BaseSpectator (see snippet) allows us to use the injection context from any spectator object.
public runInInjectionContext<T>(cb: () => T): T {
return TestBed.runInInjectionContext(cb)
}
spectator.runInInjectionContext(() => {
const activatedRouteSnapshot = spectator.inject(ActivatedRouteSnapshot);
const routerStateSnapshot = spectator.inject(RouterStateSnapshot);
expect(functionalGuard(activatedRouteSnapshot, routerStateSnapshot)).toBe(true);
});
Some problems:
Functional guards most often use the CanActivateFn type
export declare type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
As of yet the ActivatedRouteSnapshot can be created using the ActivedRouteStub of spectator fairly easily, I have not been able to find a proper way to generate a RouterStateSnapshot.
I'll continue working on this and see if I can create something useful
(Progress in fork - https://github.com/RiskChallenger/spectator)
any progress here? 😁 let me know, if i can help somehow!
I used a bit of a hack but I was able to test functional guards and resolvers
We need to use the runInInjectionContext function from @angular/core and provide a custom injector
Example resolver
export const getProjectResolver = (
route: ActivatedRouteSnapshot,
_: RouterStateSnapshot,
): Observable<Project> => {
const projectsService = inject(ProjectsService);
const { projectId } = route.snapshot.data;
return projectsService.getProject(projectId);
};
Then in your test
// this is just so we can use the createServiceFactory
class MockClass {}
let spectator: SpectatorService<MockClass>;
let injector: Injector; // we'll create a new instance of this an use spectator as the resolver
const createService = createServiceFactory({
service: MockClass,
mocks: [ProjectsService, ActivatedRouteSnapshot, RouterStateSnapshot],
});
beforeEach(() => {
spectator = createService();
// Create a new instance of injector and resolve the dependencies using spectator
injector = Injector.create({
providers: [
{
provide: ProjectsService,
useValue: spectator.inject(ProjectsService),
},
],
});
});
it('should return route project', async () => {
const route = spectator.inject(ActivatedRouteSnapshot);
const state = spectator.inject(RouterStateSnapshot);
route.data = {
projectId: 1,
};
spectator
.inject(ProjectsService)
.getProject.mockReturnValue(of({ id: 1, name: 'test' }));
// this is where the magic happens
const project = await firstValueFrom(runInInjectionContext(injector, () => getProjectResolver(route, state)));
expect(project).toBeTruthy();
expect(project.name).toBe('test');
});
I think this could be wrapped into a nice createInjectorFactory({... }) that returns a concreate Injector that can be passed into the runInInjectionContext or wrapped into an even nicer API.
What do you think @NetanelBasal ?