简体   繁体   中英

Unit testing in Angular: Mocking RxJS observable with Jasmine

I am unit testing an Angular 12 component. The component fetches, on initialisation, an observable returned from a service (see thing.service.ts below). It is assigned to a Subject, which is displayed in the html template (see app.component.html below) via the async pipe.

AppComponent.ts

export class AppComponent  {
  public errorObjectSubject = null;
  public thingsSubject: Subject<Array<IThing>> = new Subject();

  constructor(private readonly _service: ThingService) {
    this._getAllProducts();
  }

  private async _getAllProducts(): Promise<void> {
    this._service.getAllObservableAsync()
      .pipe(take(1),
        catchError(err => {
          this.errorObjectSubject = err;
          return throwError(err);
        })
      ).subscribe(result => { this.thingsSubject.next(result) });
  }
}

The template subscribes to public thingsSubject: Subject<Array<IThing>> = new Subject(); with the async pipe:

app.component.html

<div>
  <app-thing *ngFor="let thing of thingsSubject | async" [thing]="thing"></app-thing>
</div>

thing.service.ts

  constructor(private readonly _http: HttpClient) { }

  public getAllObservableAsync(): Observable<Array<IThing>> {
    return this._http.get<Array<IThing>>('https://jsonplaceholder.typicode.com/todos'); }

Here is the test setup.

app.component.spec.ts

describe('AppComponent', () => {
  let component: AppComponent,
    fixture: ComponentFixture<AppComponent>,
    dependencies: { thingService: ThingServiceStub };

  function getThings(): Array<DebugElement> {
    return fixture.debugElement.queryAll(By.directive(ThingComponentStub));
  }

  beforeEach(async () => {
    dependencies = {
      thingService: new ThingServiceStub()
    };
    await TestBed.configureTestingModule({
      declarations: [AppComponent, ThingComponentStub],
      providers: [
        { provide: ThingService, useValue: dependencies.thingService }
      ]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    //fixture.detectChanges();
  });

  describe('on initialisation', () => {
    let getThingsSubject: Subject<Array<IThing>>;

    beforeEach(() => {
      getThingsSubject = new Subject();
      (dependencies.thingService
        .getAllObservableAsync as jasmine.Spy).and.returnValue(
        getThingsSubject.asObservable()
      );
      fixture.detectChanges();
    });

    it('should fetch all of the things', () => {
      //fixture.detectChanges();
      expect(
        dependencies.thingService.getAllObservableAsync
      ).toHaveBeenCalledWith();
    });

    describe('when the things have been fetched', () => {
      beforeEach(fakeAsync(() => {
        getThingsSubject.next()
        // getThingsSubject.next([
        //   {
        //     userId: 1,
        //     id: 1,
        //     title: 'string',
        //     completed: 'string'
        //   }
        // ]);
        //getThingsSubject.pipe().subscribe()

        tick();

        fixture.detectChanges();
      }));

      it('should display the things', () => {
        expect(getThings()[0].componentInstance.product).toEqual({
          name: 'product',
          number: '1'
        });
      });
    });
  });
});

thing.service.stub.ts

export class ProductServiceStub {
    public getAllObservableAsync: jasmine.Spy = jasmine.createSpy('getAllObservableAsync');
  }

I am trying to test how it works after the template has been populated with things ( IThing[] ). I have a passing spec which calls the mock observable:

it('should fetch all of the things', () => {
  expect(
    dependencies.thingService.getAllObservableAsync
  ).toHaveBeenCalledWith();
});

However I hit the 'Error: Uncaught (in promise): TypeError: Cannot read property 'pipe' of undefine' when I attempt to test the template: describe('when the things have been fetched' .

I am not quite sure what is the problem. Is it how I have setup the subscription to the Subject? Or the change detection?

I think the order in which you're calling things can be an issue.

Try this:

describe('AppComponent', () => {
  let component: AppComponent,
    fixture: ComponentFixture<AppComponent>,
    dependencies: { thingService: ThingServiceStub };
  

  function getThings(): Array<DebugElement> {
    return fixture.debugElement.queryAll(By.directive(ThingComponentStub));
  }

  beforeEach(async () => {
    dependencies = {
      thingService: new ThingServiceStub()
    };
    await TestBed.configureTestingModule({
      declarations: [AppComponent, ThingComponentStub],
      providers: [
        { provide: ThingService, useValue: dependencies.thingService }
      ]
    }).compileComponents();
  });

  beforeEach(() => {
    // !! This (.createComponent) is when the constructor is called, so mock the observable
    // before here and change the subject to a BehaviorSubject.
    // Maybe subject will work as well.
    let getThingsSubject = new BehaviorSubject([{ name: 'product', number: '1' }]);
    (dependencies.thingService.getAllObservableAsync as jasmine.Spy).and.returnValue(
       getThingsSubject.asObservable()
    );
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
  });

  describe('on initialisation', () => {
    let getThingsSubject: Subject<Array<IThing>>;

    it('should fetch all of the things', () => {
      expect(
        dependencies.thingService.getAllObservableAsync
      ).toHaveBeenCalledWith();
    });

    describe('when the things have been fetched', () => {
      // maybe tick and fakeAsync are not needed but `fixture.detectChanges` is
      beforeEach(fakeAsync(() => {

        tick();

        fixture.detectChanges();
      }));

      it('should display the things', () => {
        // !! Add this log for debugging
        console.log(fixture.nativeElement);
        expect(getThings()[0].componentInstance.product).toEqual({
          name: 'product',
          number: '1'
        });
      });
    });
  });
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM