Scribbler
Scribbler

Reputation: 105

Angular test spy not being called

I'm having trouble configuring my Angular test to work properly. The specific issue is that my spy does not seem to be working. I'm new to Angular and trying to understand how to write tests. The background is that this is for my first Angular app (using latest version of the CLI, 7.x). It is a simple slideshow. The slideshow works fine, but I'm just trying to get the tests to work.

The first part of this is that the app needs to get the href from the JavaScript window.location. Based on suggestions on Stackoverflow and elsewhere I've created a class to wrap window so that it can be tested. Here's what that looks like ins a service called windowobject.service.ts:

import { Injectable } from '@angular/core';

function getWindow (): any {
  return window;
}

@Injectable({
  providedIn: 'root'
})
export class WindowObjectService {

  constructor() { }

  get browserWindow(): any {
    return getWindow();
  }
}

This works fine, but the issue is mocking it in the test. My test is called photos.component.spec.ts:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { PhotosComponent } from './photos.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' ;
import { WindowObjectService } from '../services/windowobject.service';
import { environment } from '../../environments/environment';

const expectedId = 916;

class MockWindowObjectService {
  browserWindow(): any {
    return { window: {
                        location: {
                            href: environment.baseUrl + '/angular/slideshow/index.html?id=' + expectedId
                        }
                      }
    };
  }
}

describe('PhotosComponent', () => {
  let component: PhotosComponent;
  let fixture: ComponentFixture<PhotosComponent>;
  let windowService: MockWindowObjectService;
  let windowSpy;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule, NgbModule ],
      declarations: [PhotosComponent],
      providers: [ { provide: WindowObjectService, useClass: MockWindowObjectService } ]
    })
    .compileComponents().then(() => {

      windowService = TestBed.get(WindowObjectService);
      fixture = TestBed.createComponent(PhotosComponent);
      component = fixture.componentInstance;
    });
  }));

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should call window object service', () => {
    windowSpy = spyOn(windowService, 'browserWindow').and.callThrough();
    expect(windowService.browserWindow).toHaveBeenCalled();
  });

});

What I'm trying to do here is Mock the window object service and determine that it has been called. Once I've got the confirmation that it has been called successfully the next test I would want to write is to confirm that it is returning the mocked value. I've tried a lot of different configurations of this test, this is just the latest iteration, but nothing seems to work. I only get one error and it is about the spy not getting called. Any ideas how to fix this and get the mock & spy working?

Update, adding photos.component.ts. This uses ng-bootstrap carousel & pagination to display a slide show that shows one image at a time and can be navigated with either the carousel arrows or the pagination component. The photo collection id is from a query string value in the url.

import { Component, OnInit, ViewChild } from '@angular/core';
import { PhotosService } from '../services/photos.service';
import { IPhoto } from '../models/photo';
import { NgbCarousel, NgbCarouselConfig, NgbPaginationConfig  } from '@ng-bootstrap/ng-bootstrap';
import { WindowObjectService } from '../services/windowobject.service';

@Component({
  selector: 'app-photos',
  templateUrl: './photos.component.html',
  styleUrls: ['./photos.component.scss'],
  providers: [NgbCarouselConfig, WindowObjectService]
})
export class PhotosComponent implements OnInit {

  // reference to "photosCarousel"
  @ViewChild('photosCarousel') photosCarousel: NgbCarousel;

  private _photosService: any;
  private _windowService: WindowObjectService;

  errorMessage: string;
  photos: IPhoto[] = new Array;
  page = 1;
  collectionSize: number;
  tripReportId: string;

  constructor(carouselConfig: NgbCarouselConfig, paginationConfig: NgbPaginationConfig, phototsService: PhotosService,
              windowService: WindowObjectService) {

    carouselConfig.showNavigationArrows = true;
    carouselConfig.showNavigationIndicators = false;
    carouselConfig.interval = 0; // Amount of time in milliseconds before next slide is shown.
    carouselConfig.wrap = false;

    paginationConfig.pageSize = 1;
    paginationConfig.maxSize = 5;
    paginationConfig.size = 'sm';
    paginationConfig.boundaryLinks = false;

    this._photosService = phototsService;
    this._windowService = windowService;
  }

  ngOnInit() {

    console.log('this._windowService.browserWindow.location.href', this._windowService.browserWindow.location.href);

    this.tripReportId = this._windowService.browserWindow.location.href.split('?')[1].split('=')[1];

    this._photosService.getPhotos(this.tripReportId).subscribe(
      photos => {
        this.photos = photos;
        this.collectionSize = photos.length;
      },
      error => this.errorMessage = <any>error
    );

    this.collectionSize = this.photos.length;
  }

  pageChanged(pageNumber: number): void {

    this.photosCarousel.select(pageNumber.toString());
  }

  public onSlide(slideData) {
    this.page = slideData.current;
  }
}

Upvotes: 1

Views: 3923

Answers (1)

dmcgrandle
dmcgrandle

Reputation: 6070

Ok, Here is what I found when I put your code into this StackBlitz.

  • As I suspected, you needed fixture.detectChanges() to be called in order for ngOnInit() to be executed.
  • Because you have WindowObjectService declared in the providers array of the @Component decorator, you cannot replace with a mock in the default providers array. Rather you need to use overrideComponent() as shown in the StackBlitz.
  • Your component sets windowService to a component variable in the constructor. Because of that it makes this very difficult to mock with a class, so I changed the mock to an object.
  • Again because you set a variable up in the constructor, the spyOnProperty() needs to be done very early, before the component is created.
  • if you spy on windowService.browserWindow directly it will cause an issue, so use the spy variable that was set up.
  • I also showed how to mock PhotosService with some data to get you started on that as well.

As you can see, the tests are now passing. I hope this helps.

Upvotes: 2

Related Questions