Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Testing for functional guards #640

Open
milo526 opened this issue Feb 13, 2024 · 4 comments
Open

Testing for functional guards #640

milo526 opened this issue Feb 13, 2024 · 4 comments

Comments

@milo526
Copy link

milo526 commented Feb 13, 2024

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.

@NetanelBasal
Copy link
Member

You are welcome to play with some ideas you have and see what's the best fit and open a PR.

@milo526
Copy link
Author

milo526 commented Feb 22, 2024

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)

@nicolaric-akenza
Copy link

any progress here? 😁
let me know, if i can help somehow!

@kornect
Copy link

kornect commented Jul 2, 2024

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 ?

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants