Search code examples
karma-jasmineangular9

Subscribe to activated route parameter in Angular9 unit test


I am hitting a problem unit testing the following Angular 9 component:

export class BirdsDetailComponent implements OnInit {
  ...

  constructor(private route: ActivatedRoute) {
    route.params.subscribe(_ => {
      this.route.paramMap.subscribe(pmap => this.getData(+pmap.get('id')));
    });
  }

  ngOnInit() {}

  getData(id: number): void { ... }

Unlike in other components that I am familiar with, this component initiates the data retrieval from the constructor.

I am hitting a problem (I think) with setting the route parameter. I have tried a range of ways of setting up the activated route mock/stub. Including the one found in this setup in the Angular docs.

describe('BirdsDetailComponent', () => {

  let component: BirdsDetailComponent;
  let fixture: ComponentFixture<BirdsDetailComponent>;

  let activatedRoute: ActivatedRouteStub;

  beforeEach(() => {
    activatedRoute = new ActivatedRouteStub();
  });


  // the `id` value is irrelevant because ignored by service stub
  beforeEach(() => activatedRoute.setParamMap({ id: 99999 }));

  beforeEach(async(() => {
    const routerSpy = createRouterSpy();
    function createRouterSpy() {
      return jasmine.createSpyObj('Router', ['navigate']);
    }

    TestBed.configureTestingModule({
      imports: [RouterTestingModule.withRoutes([])],
      declarations: [BirdsDetailComponent],
      providers: [
        {
          provide: ActivatedRoute, useValue: ActivatedRouteStub
        }
        // {
        //   provide: ActivatedRoute,
        //   useValue: {
        //     params: of({ id: '123' })
        //   }
        // }
      ]
    })
      .compileComponents();
  }));


  beforeEach(() => {
    fixture = TestBed.createComponent(BirdsDetailComponent);
    component = fixture.componentInstance;

    activatedRoute.setParamMap({ id: 1 });
  });

  it('should call getData', () => {
    const conserveStatus = {
      conservationStatusId: 1, conservationList: 'string',
      conservationListColourCode: 'string', description: 'string', creationDate: 'Date | string',
      lastUpdateDate: 'Date | string', birds: []
    };

    const bird = {
      birdId: 1, class: 'string', order: 'string', family: 'string',
      genus: 'string', species: 'string', englishName: 'string', populationSize: 'string',
      btoStatusInBritain: 'string', thumbnailUrl: 'string', songUrl: 'string',
      birderStatus: 'string', birdConservationStatus: conserveStatus,
      internationalName: 'string', category: 'string', creationDate: 'Date | string',
      lastUpdateDate: 'Date | string'
    };

    fixture.detectChanges();
    // // Act or change
    // component.getBird(1);

    // // fixture.detectChanges();

    // Assert
    expect(mockBirdsService.getBird).toHaveBeenCalled();
  });

});

I have also tried a simple setup of the activated route parameter, which you can see in the commented out code, like this (and variation of this):

 {
   provide: ActivatedRoute,
   useValue: {
     params: of({ id: '123' })
   }
 }

Whatever I try I also seem to hit the 'Cannot read property 'subscribe' of undefined thrown' error. Could the problem be related to detecting the changes? Can anyone point me in the right direction?


Solution

  • To provide the route argument in tests provide ActivatedRoute with the Map method:

    providers: [{
        provide: ActivatedRoute,
        useValue: {
            paramMap: of(new Map(Object.entries({
                id: '34' // or use a variable (better): fakeRouteArgument
            })))
        }
    }],
    

    The argument needs to be a 'Map' object otherwise "map.get is not a function" error occurs. See TypeError: map is not a function in JavaScript for a full discussion.

    Information: The above test spec setup is for when the component.ts is setup like this (subscription in ngOnInit):

      ngOnInit(): void {
        this._route.paramMap.subscribe(pmap => {
          this._getData(pmap.get('id'));
        });
      }
    

    or like this (subscription in the constructor):

      constructor(private readonly _route: ActivatedRoute) {
        this._route.paramMap.subscribe(pmap => {
          this._getData(pmap.get('id'));
        });
      }