50

I've run into missing <router-outlet> messages in other unit tests, but just to have a nice isolated example, I created an AuthGuard that checks if a user is logged in for certain actions.

This is the code:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    if (!this.authService.isLoggedIn()) {
        this.router.navigate(['/login']);
        return false;
    }
    return true;
}

Now I want to write a unit test for this.

This is how I start my test:

beforeEach(() => {
    TestBed.configureTestingModule({
        imports: [
            RouterTestingModule.withRoutes([
                {
                    path: 'login',
                    component: DummyComponent
                }
            ])
        ],
        declarations: [
            DummyComponent
        ],
        providers: [
            AuthGuardService,
            {
                provide: AuthService,
                useClass: MockAuthService
            }
        ]
    });
});

I created a DummyComponent that does nothing. Now my test. Pretend that the service returns false and that it triggers this.router.navigate(['/login']):

it('should not let users pass when not logged in', (): void => {
    expect(authGuardService.canActivate(<any>{}, <any>{})).toBe(false);
});

This will throw an exception with "Cannot find primary outlet to load". Obviously I can use toThrow() instead of toBe(false), but that doesn't seem like a very sensible solution. Since I'm testing a service here, there is no template where I can put the <router-outlet> tag. I could mock the router and make my own navigate function, but then what's the point of RouterTestingModule? Perhaps you even want to check that navigation worked.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
Bart
  • 769
  • 1
  • 5
  • 9
  • Hi can you provide your the spec file of login guard. It would be helpful to me to start writing the test cases for it. – Sumit Khanduri Dec 19 '16 at 08:50
  • 2
    For what it's worth, I share your frustration. Can't find one single example of `RouterTestingModule` that actually works... – Adam Hughes Nov 09 '17 at 21:49

6 Answers6

88

I could mock the router and make my own navigate function, but then what's the point of RouterTestingModule? Perhaps you even want to check that navigation worked.

There's no real point. If his is just a unit test for the auth guard, then just mock and spy on the mock to check that it's navigate method was called with the login argument

let router = {
  navigate: jasmine.createSpy('navigate')
}

{ provide: Router, useValue: router }

expect(authGuardService.canActivate(<any>{}, <any>{})).toBe(false);
expect(router.navigate).toHaveBeenCalledWith(['/login']);

This is how unit tests should normally be written. To try to test any actual real navigation, that would probably fall under the umbrella of end-to-end testing.

user1338062
  • 11,939
  • 3
  • 73
  • 67
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • That makes sense. I just don't really understand why `RouterTestingModule` takes routes as input. I suppose for e2e tests that could work. Thanks! – Bart Oct 28 '16 at 09:02
  • 5
    You don't _have_ to configure any routes. If you just want it to compile for directives like routerLink, you can just import it _without_ calling withRoutes. For any of the links to work though, you need to configure some routes. In your case what you we missing is a component with a ``. If there's not at least one component with an outlet, routing can't function – Paul Samsotha Oct 28 '16 at 09:13
  • 2
    I literally tried about 8 other ways to get the `router.navigate` expectation to pass and this was the only way it worked! Not sure why, to be honest. Maybe it was an order of events thing in my tests that I didn't recognize until scrapping what I had and going with your answer, but anyhow, w00t! – Gregg L Apr 03 '19 at 20:57
18

If you want to test the router without mocking it you can just inject it into your test and then spy directly on the navigate method there. The .and.stub() will make it so the call doesn't do anything.

describe('something that navigates', () => {
    it('should navigate', inject([Router], (router: Router) => {
      spyOn(router, 'navigate').and.stub();
      expect(authGuardService.canActivate(<any>{}, <any>{})).toBe(false);
      expect(router.navigate).toHaveBeenCalledWith(['/login']);
    }));
  });
Chris Putnam
  • 884
  • 1
  • 10
  • 16
4

this worked for me

describe('navigateExample', () => {
    it('navigate Example', () => {
        const routerstub: Router = TestBed.get(Router);
        spyOn(routerstub, 'navigate');
        component.navigateExample();
    });
});
Florencia Cames
  • 361
  • 1
  • 9
3
     it(`editTemplate() should navigate to template build module with query params`, inject(
        [Router],
        (router: Router) => {
          let id = 25;
          spyOn(router, "navigate").and.stub();
          router.navigate(["/template-builder"], {
            queryParams: { templateId: id }
          });
          expect(router.navigate).toHaveBeenCalledWith(["/template-builder"], {
            queryParams: { templateId: id }
          });
        }
      ));
TheTom
  • 298
  • 3
  • 15
Priti jha
  • 145
  • 1
  • 4
3

I came up with something like that:

describe('TestComponent', () => {
  let component: TestComponent;
  let router: Router;
  let fixture: ComponentFixture<TestComponent>;
  const routerSpy = jasmine.createSpyObj('Router', ['navigate']); // create a router spy


  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule
      ],
      declarations: [TestComponent],
      providers: [
        { provide: Router, useValue: routerSpy } // use routerSpy against Router
      ],
    }).compileComponents();
  }));

  beforeEach(() => {
    router = TestBed.inject(Router); // get instance of router 
    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it(`should navigate to 'home' page`, () => {
    component.navigateToHome(); // call router.navigate
    const spy = router.navigate as jasmine.Spy; // create the navigate spy
    const navArgs = spy.calls.first().args[0]; // get the spy values
    expect(navArgs[0]).toBe('/home');
  });
});

Inspired with angular docs: https://angular.io/guide/testing-components-scenarios#routing-component

Experimenter
  • 2,084
  • 1
  • 19
  • 26
0

I am new to unit testing angular/javascript apps. I needed a way to mock (or spy) for my unit test. The following line is borrowed from Experimenter and helped me TREMENDOUSLY!

const routerSpy = jasmine.createSpyObj('Router', ['navigate']); // create a router spy

I would like to say that I had no idea I could do that with Jasmine. Using that line above, allowed me to then create a spy on that object and verify it was called with the correct route value.

This is a great way to do unit testing without the need to have the testbed and all the ceremony around getting the testing module setup. Its also great because it still allows me to have a fake router object with out the need to stub in all of the parameters, methods, etc etc etc.