Orodan
Orodan

Reputation: 1047

Testing observables fail without detectChanges

I was reproducing some examples of the Angular documentation to improve my understanding of angular unit tests, and I ended up in a simple test case when I can't figure out what's going on.

Here is my app.component.ts file when I have a single method "getQuote" that get a quote from a service.

@Component({...})
export class AppComponent {
  errMsg: string;
  quote: Observable<string>;

  constructor (private twainService: TwainService) {}

  getQuote () {
    this.quote = this.twainService.getQuote().pipe(
      catchError(err => {
        this.errMsg = err;
        return of('...');
      })
    );
  }
}

Then, I created a test to verify if my errMsg prop was correctly updated in case I got an error from my twainService.getQuote method :

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

  let getQuoteSpy;

  beforeEach(async(() => {
    const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
    getQuoteSpy = twainService.getQuote.and.returnValue(of(testQuote));

    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      providers: [
        { provide: TwainService, useValue: twainService }
      ]
    }).compileComponents();
  }));

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

  it('should get error msg when TwainService fails', async(async () => {
    const errMsg = 'TwainService fails';
    getQuoteSpy.and.returnValue(throwError(errMsg));

    component.getQuote();
    await fixture.whenStable();

    expect(component.errMsg).toBe(errMsg);
  }));
});

But here is the problem : this test always fails, and I can't get to see what's wrong.

Playing around, I managed to find that adding a "fixture.detectChanges()" like the following was making the test works, but I don't understand why. I thought the detectChanges method was only used to propagate changes to the component view.

it('should get error msg when TwainService fails', async(async () => {
    const errMsg = 'TwainService fails';
    getQuoteSpy.and.returnValue(throwError(errMsg));

    component.getQuote();
    fixture.detectChanges();
    await fixture.whenStable();

    expect(component.errMsg).toBe(errMsg);
  }));

I tested with async, fakeAsync, and using synchronous observable emitting directly an error and asynchronous observable, and the result is always the same.

If anyone can help me understand what's going on there :)

Upvotes: 10

Views: 1479

Answers (1)

shokha
shokha

Reputation: 3179

I guess you simply need to subscribe to your quote Observable after calling getQuote method in your tests:

 it('should get error msg when TwainService fails', async(async () => {
    const errMsg = 'TwainService fails';
    getQuoteSpy.and.returnValue(throwError(errMsg));

    component.getQuote();
    component.quote.subscribe();
    await fixture.whenStable();

    expect(component.errMsg).toBe(errMsg);
  }));

So when you call component.getQuote() in your tests - it simply sets the this.quote property 'a cold' observable and in order to see that catchError was fired, you have to subscribe to the Observable. This will run it and eventually you'll get an error according to your mocked data throwError(errMsg).

Edit

According to the second part of your question:

Playing around, I managed to find that adding a "fixture.detectChanges()" like the following was making the test works, but I don't understand why.

I've also figured it out and most probably you are using async pipe somewhere in the component's template: {{ quote | async }}. Under the hood angular's async pipe subscribes to a quote Observable and returns the latest value it has emitted. This is why we need to call detectChanges method and after this - async pipe will start subscribing to the quote Observable (and the test will work as expected). In this case we don't need to manually subscribe to quote Observable (async pipe takes care of it). And yes - you were right: I thought the detectChanges method was only used to propagate changes to the component view.

You can test both examples in the stackblitz example. Hope this will be helpful :)

Upvotes: 9

Related Questions