Mocking router.events.subscribe() Angular2

28,829

Solution 1

I have found the answer, if someone is looking for it:

import { NavigationEnd } from '@angular/router';
import { Observable } from 'rxjs/Observable';

class MockRouter {
  public ne = new NavigationEnd(0, 'http://localhost:4200/login', 'http://localhost:4200/login');
  public events = new Observable(observer => {
    observer.next(this.ne);
    observer.complete();
  });
}

class MockRouterNoLogin {
  public ne = new NavigationEnd(0, 'http://localhost:4200/dashboard', 'http://localhost:4200/dashboard');
  public events = new Observable(observer => {
    observer.next(this.ne);
    observer.complete();
  });
}

Solution 2

The accepted answer is correct but this is a bit simpler, you can replace

public ne = new NavigationEnd(0, 'http://localhost:4200/login', 'http://localhost:4200/login');
public events = new Observable(observer => {
    observer.next(this.ne);
    observer.complete();
  });

by:

public events = Observable.of( new NavigationEnd(0, 'http://localhost:4200/login', 'http://localhost:4200/login'));

And find below a full test file to test the function in the question:

import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
  async,
  TestBed,
  ComponentFixture
} from '@angular/core/testing';

/**
 * Load the implementations that should be tested
 */
import { AppComponent } from './app.component';

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


class MockServices {
  // Router
  public events = Observable.of( new NavigationEnd(0, 'http://localhost:4200/login', 'http://localhost:4200/login'));
}

describe(`App`, () => {
  let comp: AppComponent;
  let fixture: ComponentFixture<AppComponent>;
  let router: Router;

  /**
   * async beforeEach
   */
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ AppComponent ],
      schemas: [NO_ERRORS_SCHEMA],
      providers: [
        { provide: Router, useClass: MockServices },
      ]
    })
    /**
     * Compile template and css
     */
    .compileComponents();
  }));

  /**
   * Synchronous beforeEach
   */
  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    comp    = fixture.componentInstance;

    router = fixture.debugElement.injector.get( Router);

    /**
     * Trigger initial data binding
     */
    fixture.detectChanges();
  });

  it(`should be readly initialized`, () => {
    expect(fixture).toBeDefined();
    expect(comp).toBeDefined();
  });

  it('ngOnInit() - test that this.loggedIn is initialised correctly', () => {
    expect(comp.loggedIn).toEqual(true);
  });

});

Solution 3

I created a version of the router stub from Angular docs that uses this method to implement NavigationEnd event for testing:

import {Injectable} from '@angular/core';
import { NavigationEnd } from '@angular/router';
import {Subject} from "rxjs";

@Injectable()
export class RouterStub {
  public url;
  private subject = new Subject();
  public events = this.subject.asObservable();

  navigate(url: string) {
    this.url = url;
    this.triggerNavEvents(url);
  }

  triggerNavEvents(url) {
    let ne = new NavigationEnd(0, url, null);
    this.subject.next(ne);
  }
}

Solution 4

This is a really old question but I just came across it looking for something better than what i have and in my case i need to test several different events. My basic approach was just to change the Router.events to a non read only value like

 (router as any).events = new BehaviorSubject<any>(null);
 fixture.detectChanges();
 router.events.next(new NavigationEnd(0, 'http://localhost:4200/login', 
 'http://localhost:4200/login'));
  expect(comp.loggedIn).toEqual(true);

Hope maybe that helps someone. I couldn't find an easier solution after looking around

Solution 5

  • 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
  • then add the tests for the component, I think that will be trivial right :)

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 routerEventReplaySubject: ReplaySubject<RouterEvent>;
  let routerMock;

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

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

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

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

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

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

    it('NavigationStart', () => {
      const result: Observable<string> = service.subscribeToRouterEventUrl();
      const url = '/mock';

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

      routerEventReplaySubject.next(new NavigationStart(1, url, 'imperative', null));
    });
  });
});
Share:
28,829
stijn.aerts
Author by

stijn.aerts

A newbie in android development, and a littles less of a newbie in Java. Very eager to learn!

Updated on February 17, 2022

Comments

  • stijn.aerts
    stijn.aerts about 2 years

    In my app.component.ts I have the following ngOnInit function:

    ngOnInit() {
        this.sub = this.router.events.subscribe(e => {
          if (e instanceof NavigationEnd) {
            if (!e.url.includes('login')) {
              this.loggedIn = true;
            } else {
              this.loggedIn = false;
            }
          }
        });
      }
    

    Currently I'm testing if the sub is not null but I want to test the function with a 100% coverage.

    I want to mock the router object so that I can simulate the URL and then test if the this.loggedIn is correctly set.

    How would I proceed to mock this function? I tried it but I don't know how I would take this on with the callback involved and with the NavigationEnd.

  • Amit Chigadani
    Amit Chigadani about 7 years
    Please provide info on your TestingModule Configuration.
  • DAG
    DAG almost 7 years
    Do you have any clue how to mock RoutesRecognized instead of NavigationEnd?
  • kat1330
    kat1330 over 6 years
    This answer is incomplete. Please provide explanation of usage.
  • Reddi Rajendra P
    Reddi Rajendra P almost 6 years
    Thanks for providing the full file. Helpful
  • Syed Jafri
    Syed Jafri almost 6 years
    To get this to work provide the mock class to the TestBed. providers: [{provide: Router, useClass: MockRouter]
  • Neurotransmitter
    Neurotransmitter almost 4 years
    This was actually helpful.