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 :)
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.
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.
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.
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).
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 :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With