57

Let's say I have this

export class QuestionnaireQuestionsComponent {

    questions: Question[] = [];
    private loading:boolean = true;


    constructor(
        private route: ActivatedRoute,
        public questionnaireService:QuestionnaireService) {}

    ngOnInit(){
        this.route.parent.params.subscribe((params:any)=>{
            this.questionnaireService.getQuestionsForQuestionnaire(params.id).subscribe((questions)=>{
                this.questions = questions;
                this.loading = false;
            });
        });
    }


}

My component is actually working pretty well. Problem is that I want to unit test it but I can't figure out how to mock the this.route.parent object. Here's my test that fails

beforeEach(()=>{
    route = new ActivatedRoute();
    route.parent.params = Observable.of({id:"testId"});

    questionnaireService = jasmine.createSpyObj('QuestionnaireService', ['getQuestionsForQuestionnaire']);
    questionnaireService.getQuestionsForQuestionnaire.and.callFake(() => Observable.of(undefined));
    component = new QuestionnaireQuestionsComponent(route, questionnaireService);
});


describe("on init", ()=>{
    it("must call the service get questions for questionnaire",()=>{
        component.ngOnInit();
        expect(questionnaireService.getQuestionsForQuestionnaire).toHaveBeenCalled();
    });  
});

The test fails with this error

TypeError: undefined is not an object (evaluating 'this._routerState.parent') 
S.Galarneau
  • 2,194
  • 1
  • 24
  • 26

8 Answers8

58

Using TestBed

 beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [YourComponent],
      imports: [],
      providers: [
        {
          provide: ActivatedRoute, useValue: {
            params: Observable.of({ id: 'test' })
          }
        }
      ]
    })
      .compileComponents();
  }));
Kevin Black
  • 886
  • 1
  • 8
  • 7
30

AcitvatedRoute is an interface according to angular2 docs, so what I did is implements a MockActivatedRoute

import {Observable} from 'rxjs';
import {Type} from '@angular/core';
import {ActivatedRoute,Route,ActivatedRouteSnapshot,UrlSegment,Params,Data } from '@angular/router';

export class MockActivatedRoute implements ActivatedRoute{
    snapshot : ActivatedRouteSnapshot;
    url : Observable<UrlSegment[]>;
    params : Observable<Params>;
    queryParams : Observable<Params>;
    fragment : Observable<string>;
    data : Observable<Data>;
    outlet : string;
    component : Type<any>|string;
    routeConfig : Route;
    root : ActivatedRoute;
    parent : ActivatedRoute;
    firstChild : ActivatedRoute;
    children : ActivatedRoute[];
    pathFromRoot : ActivatedRoute[];
    toString() : string{
        return "";
    };
}

and just replace the ActivatedRoute in my tests for MockActivatedRoute like this

beforeEach(()=>{
    route = new MockActivatedRoute();
    route.parent = new MockActivatedRoute();
    route.parent.params = Observable.of({id:"testId"});

    questionnaireService = jasmine.createSpyObj('QuestionnaireService', ['getQuestionsForQuestionnaire']);
    questionnaireService.getQuestionsForQuestionnaire.and.callFake(() => Observable.of(undefined));
    component = new QuestionnaireQuestionsComponent(route, questionnaireService);
});
S.Galarneau
  • 2,194
  • 1
  • 24
  • 26
  • Thank you for this, it is very helpful even for activated routes where you aren't concerned about the parent. Question: Is there a reason you prefer to new-up the component rather than inject the route and service? – glitchbane Feb 05 '17 at 17:11
  • Just because im not testing the component behavior... so no need to inject it really, the component shouldn't change anything in my test cases – S.Galarneau Feb 06 '17 at 16:03
  • You can also refer to the docs: https://angular.io/docs/ts/latest/testing#!#create-an-_observable_-test-double – peinearydevelopment Mar 03 '17 at 19:46
  • They changed docs for test double: [https://angular.io/docs/ts/latest/testing#!#stub-observable](https://angular.io/docs/ts/latest/testing#!#stub-observable) – Markus May 22 '17 at 13:27
  • 1
    Both of those testing doc links are broken :( – Splaktar Sep 29 '17 at 22:32
  • 3
    I got cannot assign 'parent' because it is constant or read-only property. Also there is missing props and interface: paramMap and queryParamMap. What should put for those, and will this work in Anuglar 4? – IntoTheDeep Nov 10 '17 at 09:23
  • If you want to go down this route you should use a mocking framework like typemoq. Soemthing like `foo = Mock.ofType()`, `foo.setup(r => r.queryParams).returns(() => { key: "whatever" }` – RJFalconer Dec 05 '18 at 09:46
19

For anyone new to this question the angular docs cover this scenario.

As per documentation above:

Create ActivatedRouteStub class to be used as test double for ActivatedRoute

import { ReplaySubject } from 'rxjs/ReplaySubject';
import { convertToParamMap, ParamMap, Params } from '@angular/router';

/**
 * An ActivateRoute test double with a `paramMap` observable.
 * Use the `setParamMap()` method to add the next `paramMap` value.
 */
export class ActivatedRouteStub {
  // Use a ReplaySubject to share previous values with subscribers
  // and pump new values into the `paramMap` observable
  private subject = new ReplaySubject<ParamMap>();

  constructor(initialParams?: Params) {
    this.setParamMap(initialParams);
  }

  /** The mock paramMap observable */
  readonly paramMap = this.subject.asObservable();

  /** Set the paramMap observables's next value */
  setParamMap(params?: Params) {
    this.subject.next(convertToParamMap(params));
  };
}

And then use the stub in the test class as follows:

activatedRoute.setParamMap({ id: '1234' });
Eneko
  • 1,709
  • 1
  • 16
  • 25
Garreth Golding
  • 985
  • 2
  • 11
  • 19
6

To customize a mocked ActivatedRoute's data inside each 'it' block, combine what Kevin Black suggested above

beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [YourComponent],
      imports: [],
      providers: [
        {
          provide: ActivatedRoute, useValue: {
            queryParams: of({ id: 'test' })
          }
        }
      ]
    })
      .compileComponents();
  }));

and the below code in the it('') block before instantiating a component using fixture.detectChanges()

it('test', fakeAsync(() => {
  let activatedRoute: ActivatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
  activatedRoute.queryParams = of({ firstName: 'john', lastName: 'smith' });

  fixture.detectChanges(); // trigger ngOninit()
  tick();
}));
6

For scenarios in which you are reading the query params from the ActivatedRouteSnapshot like:

this.someProperty = this.route.snapshot.data.query['paramKey'];

The below code will be helpful in tests for feeding query params data into the route

{ 
  provide: ActivatedRoute, 
  useValue: {
    snapshot: {
      data: {
        query: {
          paramKey: 'paramValue'
        }
      }
    }
  }
}
Muneeb
  • 151
  • 2
  • 5
3

You can also simple plass {params: {searchTerm: 'this is not me'}} as any) as ActivatedRouteSnapshot

detailds code

  (service.resolve(({params: {id: 'this is id'}} as any) as ActivatedRouteSnapshot,
    {} as RouterStateSnapshot)as Observable<xyzResolveData>)
    .subscribe((data) => {
      expect((data as xyzResolveData).results).toEqual(xyzData.results);
    });
Aniruddha Das
  • 20,520
  • 23
  • 96
  • 132
2

Angular 9 version

  1. Create your mock:

     const mockActivatedRoute = { 
       parent: { 
         paramMap: of(convertToParamMap(
             { organization: 'fakeOrganization' }
         ))}
     };
    
  2. Provide it as the Activated Route

      providers: [{ provide: ActivatedRoute, useValue: mockActivatedRoute }]
    
  3. Test it

     describe('ngOnInit', () => {
         it('should get the route parameter', fakeAsync(() => {
           component.ngOnInit();
    
           expect(component.organization).toBe('fakeOrganization');
         }));
       });'
    
  4. Code being tested

       ngOnInit(): void {
         this.route.parent.paramMap.subscribe(params => {
           this.organization = params.get('organization');
         });
       }
    
0

Building off of @S.Galarneau 's accepted answer, here is the updated ActivatedRoute interface (angular 4+), with access modifiers, only using a handful of types, as well as using the providers array and integrating with TestBed:

This works great to create a mock interface and use the providers array to use your version of ActivatedRoute:

export class MockActivatedRoute implements ActivatedRoute{
  public snapshot;
  public url;
  public params;
  public queryParams: Observable<Params>;
  public fragment: Observable<string>;
  public data;
  public outlet;
  public component;
  public paramMap;
  public queryParamMap;
  public routeConfig;
  public root;
  public parent;
  public firstChild;
  public children;
  public pathFromRoot;
  public toString(): string {
    return '';
  };
}

And use with TestBed and Providers array, only dealing with the fragment property, like this:

  beforeEach(async((): void => {
    const route = new MockActivatedRoute();
    route.fragment = of('#myhash?some_params=some_value'); 
    // because fragment is an Observable, we need `of(value)`
    

  TestBed.configureTestingModule({
    // declarations, imports, and providers go next:
    providers:[
        { provide: ActivatedRoute, useValue: route},
    ]
  })
Danny
  • 3,982
  • 1
  • 34
  • 42