简体   繁体   中英

How to test a subject and observable return value in Angular Jasmine Spec?

In Angular 9, I have a loader service like below.

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

export interface LoaderState {
  show: boolean;
}

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

  private loaderSubject = new Subject<LoaderState>();

  loaderState = this.loaderSubject.asObservable();

  constructor() { }

  show() {
      this.loaderSubject.next({show: true} as LoaderState);
  }

  hide() {
      this.loaderSubject.next({show: false} as LoaderState);
  }
}

I want to test the show() and hide() methods. For that, I have written a spec like below.

it('should show', (done) => {
      spyOn(loaderService, 'show').and.callThrough();
      loaderService.show();
      expect(loaderService.show).toHaveBeenCalled();
      loaderService.loaderState.subscribe((state) => {
          expect(state).toBe({show: true});
          done();
      });
  });

But I get below error when I run this

Chrome 86.0.4240 (Windows 10.0.0) LoaderService should show FAILED Error: Timeout - Async callback was not invoked within 5000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)

I have searched a lot before asking this question. But can not seem to find the right solution. I am new to Jasmin Unit testing. Any help is appreciated.

Edit: Posting complete spec file for reference.

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

import { LoaderComponent } from './loader.component';
import { LoaderService } from './loader.service';

describe('LoaderService', () => {
  let component: LoaderComponent;
  let fixture: ComponentFixture<LoaderComponent>;
  let loaderService: LoaderService;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ LoaderComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(LoaderComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    loaderService = TestBed.inject(LoaderService);
  });

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

  it('should show', (done) => {
      spyOn(loaderService, 'show').and.callThrough();
      loaderService.show();
      expect(loaderService.show).toHaveBeenCalled();
      loaderService.loaderState.subscribe((state) => {
          expect(state).toBe({show: true});
          done();
      });
  });
});

The problem is that you're calling subscribe after the actual method call. So when you call show(); there is nothing subscribed to that event, and since the done() callback is inside of it... it's never called.

it('should show', (done) => {
    service.loaderState.subscribe((state) => { //subscribe first
      expect(state).toEqual({show: true}); // change toEqual instead of toBe since you're comparing objects
      done();
      });

    spyOn(service, 'show').and.callThrough();
    service.show(); // invoke after
    expect(service.show).toHaveBeenCalled();
  });

Here's my solution to this pattern:

Service Class

import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';

@Injectable({

  providedIn: 'root'

})

export class MyService {

  private myServiceEvent = new Subject<any>();

  constructor() { }

  success(): Observable<any> {
    return this.myServiceEvent.asObservable();
  }
  
  doSomething(someData: any): void {
    // stuff
    this.myServiceEvent.next(someData);
  }
}

Component that uses the service (the observable would be triggered by some other component or service calling the doSomething method on the service)

import { Component, OnInit } from '@angular/core';
import { MyService } from ./my-service.ts

@Component({
  selector: 'app-mycomponent',
  templateUrl: './app-mycomponent.component.html',
  styleUrls: ['./app-mycomponent.component.scss']
})
export class AppMyComponent implements OnInit {

    private test = '';

  constructor(
    private myService: MyService,
  ) {  }

  ngOnInit() {
    this.myService.success().subscribe(data => {
        test = data;
    });
  }
}

Spec that tests the component using the service's Observable response. Note the construction of the service mock and how the Subject variable is used to trigger the Observable.

import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { Observable, Subject } from 'rxjs';

import { AppMyComponent } from './app-my.component';
import { MyService } from ./my-service.ts

describe('AppMyComponent', () => {
  let component: AppMyComponent;
  let fixture: ComponentFixture<AppMyComponent>;

  const myServiceSubject = new Subject<any>();

  const myServiceMock = jasmine.createSpyObj('MyService', [], {
    success: () => myServiceSubject.asObservable()
  });
  
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      schemas: [ CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
      declarations: [ AppMyComponent ],
      providers: [
        { provide: MyService, useValue: myServiceMock }
      ]
    })
    .compileComponents();
  }));

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

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

  it('should execute subscription to MyService success event', fakeAsync(() => {
    myServiceSubject.next('somedata');
    tick();
    expect(componenet.test).toEqual('somedata');
  }));

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