Angular 6 - how to mock router.events url response in unit test
Asked Answered
M

7

22

As the title indicates, I need to mock router.events in a unit test.

In my component, I doing some regex to grab the first instance of text between slashes in the url; e.g., /pdp/

  constructor(
    private route: ActivatedRoute,
    private router: Router,
  }

this.router.events.pipe(takeUntil(this.ngUnsubscribe$))
      .subscribe(route => {
        if (route instanceof NavigationEnd) {
        // debugger
          this.projectType = route.url.match(/[a-z-]+/)[0];
        }
      });

My unit tests error out when the component is being built: Cannot read property '0' of null. When I comment-in the debugger, route does not seem to be set correctly, yet I am setting it in the unit test itself. I have done it several different ways (inspired by this post: Mocking router.events.subscribe() Angular2 and others).

First attempt:

providers: [
        {
          provide: Router,
          useValue: {
            events: of(new NavigationEnd(0, '/pdp/project-details/4/edit', 'pdp/project-details/4/edit'))
          }
        },
        // ActivatedRoute also being mocked
        {
          provide: ActivatedRoute,
          useValue: {
            snapshot: { url: [{ path: 'new' }, { path: 'edit' }] },
            parent: {
              parent: {
                snapshot: {
                  url: [
                    { path: 'pdp' }
                  ]
                }
              }
            }
          }
        }
]

Second attempt (based on the above post):

class MockRouter {
  public events = of(new NavigationEnd(0, '/pdp/project-details/4/edit', '/pdp/project-details/4/edit'))
}

providers: [
        {
          provide: Router,
          useClass: MockRouter
        }
]

Third attempt (also based on above post):

class MockRouter {
  public ne = new NavigationEnd(0, '/pdp/project-details/4/edit', '/pdp/project-details/4/edit');
  public events = new Observable(observer => {
    observer.next(this.ne);
    observer.complete();
  });
}

providers: [
        {
          provide: Router,
          useClass: MockRouter
        }
]

Fourth attempt:

beforeEach(() => {
    spyOn((<any>component).router, 'events').and.returnValue(of(new NavigationEnd(0, '/pdp/project-details/4/edit', 'pdp/project-details/4/edit')))
...

Fifth attempt:

beforeEach(() => {
    spyOn(TestBed.get(Router), 'events').and.returnValue(of({ url:'/pdp/project-details/4/edit' }))
...

In all the above cases, route is not being set; the NavigationEnd object is equal to:

{ id: 1, url: "/", urlAfterRedirects: "/" }

Thoughts?

Mazdaism answered 19/10, 2018 at 23:24 Comment(0)
T
35

It can be as simple as:

TestBed.configureTestingModule({
  imports: [RouterTestingModule]
}).compileComponents()
    
...
    
const event = new NavigationEnd(42, '/', '/');
(TestBed.inject(Router).events as Subject<Event>).next(event);
Taurine answered 8/4, 2019 at 16:45 Comment(6)
TS2339 Property 'next' does not exist on type 'Observable<event>'Dolorisdolorita
@RichardCollette: You get this as a lining error or an actual error while executing the test? Which Angular version are you running? (in case of a liniting error, this can be ignored (cast to any)Taurine
This is a typescript compiler error, not a linting error. While the returned type of Router.events may implement both Observer and Observable interfaces, the public interface of the events property is Observable which doesn't expose next(). There is a risk of a breaking change in the future if the object returned in the future is not an Observer. Using Angular 7, TS 3.2.4Dolorisdolorita
@RichardCollette you are right about the risk of breaking changes in the future. Regarding the compile error: This I cannot understand - the TestBed.get method has return type any, hence there cannot be any compile time checks (unless you specifically cast the result to Router)..Taurine
In my Case, the subscribption in the component does get trigger by this piece of code, but, my test doesn't wait for the subscription to resolve, fixture.whenStable never resolves, I tried everything with async, fakeAsync etc ...Priebe
Only difference in Angular 17 was (TestBed.inject(Router).events as unknown as Subject<RouterEvent>).next(event); in my end. Thank you for the solution!Gorham
M
17

Here is the answer I came up with. I think I just didn't extend the first approach (above) far enough:

import { of } from 'rxjs';
import { NavigationEnd, Router } from '@angular/router';

providers: [
    {
      provide: Router,
      useValue: {
        url: '/non-pdp/phases/8',
        events: of(new NavigationEnd(0, 'http://localhost:4200/#/non-pdp/phases/8', 'http://localhost:4200/#/non-pdp/phases/8')),
        navigate: jasmine.createSpy('navigate')
      }
    }
]
Mazdaism answered 22/10, 2018 at 20:4 Comment(1)
But then this router is not able to navigate, so you cannot test The scenario where, navigating to "" should redirect to /home for instancePriebe
A
5

Thought this might help...have a footer component with routerLinks in the template. Was getting the root not provided error....had to use a "real" router...but have a spied variable..and point the router.events to my test observable so I can control nav events

export type Spied<T> = { [Method in keyof T]: jasmine.Spy };

describe("FooterComponent", () => {
   let component: FooterComponent;
   let fixture: ComponentFixture<FooterComponent>;
   let de: DebugElement;
   const routerEvent$ = new BehaviorSubject<RouterEvent>(null);
   let router: Spied<Router>;

 beforeEach(async(() => {
    TestBed.configureTestingModule({
        providers: [provideMock(SomeService), provideMock(SomeOtherService)],
        declarations: [FooterComponent],
        imports: [RouterTestingModule]
    }).compileComponents();

    router = TestBed.get(Router);
    (<any>router).events = routerEvent$.asObservable();

    fixture = TestBed.createComponent(FooterComponent);
    component = fixture.componentInstance;
    de = fixture.debugElement;
}));
// component subscribes to router nav event...checking url to hide/show a link
it("should return false for showSomeLink()", () => {
    routerEvent$.next(new NavigationEnd(1, "/someroute", "/"));
    component.ngOnInit();
    expect(component.showSomeLink()).toBeFalsy();
    fixture.detectChanges();
    const htmlComponent = de.query(By.css("#someLinkId"));
    expect(htmlComponent).toBeNull();
});
Antrim answered 13/11, 2019 at 3:33 Comment(1)
This works, but along with this solution, all my tests based on Router.navigate(...) fail. How can you Have both Router.navigate and router.event.subscribe() tested ?Priebe
K
4

I saw this alternative and it works with RouterTestingModule like @daniel-sc 's answer

   const events = new Subject<{}>();
   const router = TestBed.get(Router);
   spyOn(router.events, 'pipe').and.returnValue(events);
   events.next('Result of pipe');
Korwin answered 8/7, 2020 at 22:27 Comment(1)
get() is deprecated. From v9.0.0, using inject() is recommended.Protochordate
G
2

Hope this could help, this is what I did:

const eventsStub = new BehaviorSubject<Event>(null);
    export class RouterStub {
      events = eventsStub;
    }

    beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [AppComponent],
          imports: [RouterTestingModule],
          providers: [
            { provide: HttpClient, useValue: {} },
            { provide: Router, useClass: RouterStub },
            Store,
            CybermetrieCentralizedService,
            TileMenuComponentService,
            HttpErrorService
          ],
          schemas: [NO_ERRORS_SCHEMA]
        }).compileComponents();
        fixture = TestBed.createComponent(AppComponent);
        router = TestBed.get(Router);
        tileMenuService = TestBed.get(TileMenuComponentService);
        component = fixture.componentInstance;
      }));

and then on the test I did this:

     it('should close the menu when navigation to dashboard ends', async(() => {
        spyOn(tileMenuService.menuOpened, 'next');
        component.checkRouterEvents();
        router.events.next(new NavigationEnd (1, '/', '/'));
        expect(tileMenuService.menuOpened.next).toHaveBeenCalledWith(false);
      }));

this to verify that after the navigation to route '/' I execute an expected instruction

Granddaughter answered 26/6, 2019 at 16:46 Comment(0)
K
0

Every time I want to unit test a router event, I look at my previous code and think "surely there must be a better way". This page seems to be the most hit and has some really good code... but:

  • RouterTestingModule has been deprecated
  • Testing with the properly constructed objects makes (compliler / linting) sense
  • Source code purely there to help testing is wrong

Step 1 - Construct some decent test objects

const routerEvent$: Subject<RouterEvent> = new Subject<RouterEvent>();
const navStartEvent: NavigationStart = new NavigationStart(1, '/fakeUrl');
const navEndEvent: NavigationEnd = new NavigationEnd(1, '/fakeUrl', '/anotherFakeUrl');

Importing as neccessary from @angular/router / rxjs

Step 2 - Provide these to your unit tests

beforeEach(() => {
 TestBed.configureTestingModule({
   ...
    providers: [
      {
        provide: Router,
        useValue: { events: routerEvent$ }
      }
    ]
  });

There are other ways of doing this, but this is probably the most Angular! The quick and dirty (wrong) alternative is to add .pipe() between router.events and .subscribe() in your component.ts file. Then in your .spec.ts file you can just spyOn(router.events, 'pipe').and.returnValue(routerEvent$); - unit tests as below.

Step 3 - Trigger specific router events in unit tests

it('should do different things on navigation start / end', () => {
  expect(loadingSvc.spinnerOn).not.toHaveBeenCalled();
  expect(loadingSvc.spinnerOff).not.toHaveBeenCalled();
  routerEvent$.next(navStartEvent); // Sends the nav start event
  fixture.detectChanges();
  expect(loadingSvc.spinnerOn).toHaveBeenCalledTimes(1);
  expect(loadingSvc.spinnerOff).not.toHaveBeenCalled();
  routerEvent$.next(navEndEvent); // Sends the nav end event
  fixture.detectChanges();
  expect(loadingSvc.spinnerOn).toHaveBeenCalledTimes(1);
  expect(loadingSvc.spinnerOff).toHaveBeenCalledTimes(1);
});

Loading spinner just used as an example - better ways of doing it can be found elsewhere!

Kunkel answered 17/5 at 10:45 Comment(0)
N
-1
  • use ReplaySubject<RouterEvent> to mock the router.events
  • export it to a service so that you can test it more independently from the component, and other components may also want to use this service ;)
  • use filter instead of instanceof

source code

import {Injectable} from '@angular/core';
import {NavigationEnd, Router, RouterEvent} from '@angular/router';
import {filter, map} from 'rxjs/operators';
import {Observable} from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class RouteEventService {
  constructor(private router: Router) {
  }

  subscribeToRouterEventUrl(): Observable<string> {
    return this.router.events
      .pipe(
        filter(event => event instanceof NavigationEnd),
        map((event: RouterEvent) => event.url)
      );
  }
}

test code

import {TestBed} from '@angular/core/testing';

import {RouteEventService} from './route-event.service';
import {NavigationEnd, NavigationStart, Router, RouterEvent} from '@angular/router';
import {Observable, ReplaySubject} from 'rxjs';

describe('RouteEventService', () => {
  let service: RouteEventService;
  let routerEventRelaySubject: ReplaySubject<RouterEvent>;
  let routerMock;

  beforeEach(() => {
    routerEventRelaySubject = new ReplaySubject<RouterEvent>(1);
    routerMock = {
      events: routerEventRelaySubject.asObservable()
    };

    TestBed.configureTestingModule({
      providers: [
        {provide: Router, useValue: routerMock}
      ]
    });
    service = TestBed.inject(RouteEventService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  describe('subscribeToEventUrl should', () => {
    it('return route equals to mock url on firing NavigationEnd', () => {
      const result: Observable<string> = service.subscribeToRouterEventUrl();
      const url = '/mock';

      result.subscribe((route: string) => {
        expect(route).toEqual(url);
      });

      routerEventRelaySubject.next(new NavigationEnd(1, url, 'redirectUrl'));
    });

    it('return route equals to mock url on firing NavigationStart', () => {
      const result: Observable<string> = service.subscribeToRouterEventUrl();
      const url = '/mock';

      result.subscribe((route: string) => {
        expect(route).toBeNull();
      });

      routerEventRelaySubject.next(new NavigationStart(1, url, 'imperative', null));
    });
  });
});

Northernmost answered 12/1, 2021 at 18:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.