Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 :)

like image 650
Orodan Avatar asked Aug 22 '18 22:08

Orodan


People also ask

What is the purpose of fixture detectChanges?

Fixtures have access to a debugElement , which will give you access to the internals of the component fixture. Change detection isn't done automatically, so you'll call detectChanges on a fixture to tell Angular to run change detection.

What does detectChanges do in Angular Jasmine tests?

detectChanges() tells Angular to run change-detection. Finally! Every time it is called, it updates data bindings like ng-if, and re-renders the component based on the updated data. Calling this function will cause ngOnInit to run only the first time it is called.

What is fakeAsync in Angular?

fakeAsynclinkWraps a function to be executed in the fakeAsync zone: Microtasks are manually executed by calling flushMicrotasks() . Timers are synchronous; tick() simulates the asynchronous passage of time.

What is ComponentFixture Jasmine?

The ComponentFixture is a test harness for interacting with the created component and its corresponding element. Access the component instance through the fixture and confirm it exists with a Jasmine expectation: content_copy const component = fixture. componentInstance; expect(component).


1 Answers

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 :)

like image 187
shohrukh Avatar answered Oct 18 '22 19:10

shohrukh