Skip to content
New issue

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

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

Already on GitHub? Sign in 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 ?

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

No branches or pull requests

4 participants