Reputation: 3259
I wrote this test for my Angular app:
it('should request confirmation before deleting & abort action if user declined', fakeAsync(() => {
spyOn(appService, 'confirm').and.returnValue(of(false));
spyOn(personService, 'delete').and.callThrough();
component.deleteEntry(testPerson);
//tick(); // Missing tick()!
expect(personService.delete).not.toHaveBeenCalled();
}));
This is the component method I'm testing:
async deleteEntry(person: Person) {
if (await this.appService.confirm().toPromise()) {
return;
}
try {
await this.personService.delete(person).toPromise();
} catch(resp) {
this.appService.errorMsgBox();
}
}
(confirm()
's purpose is to show a confirmation dialog and return an Observable emitting true
/ false
depending on the user input)
If you look carefully, there is an error in my component function. I forget the !
-operator when checking the result of confirm()
. The correct code would be
if (!await this.appService.confirm().toPromise()) {
However, the test will pass. I'm not 100% sure, but I guess it passes, because the expect()
-statement at the end performs its check before confirm()
has returned its value. So yes, of course, personService.delete()
has not been called. If I uncomment the tick()
the test works as expected, and detects the error.
Now I expected fakeAsync()
to throw an error due to pending microtasks. To my suprise, it does not. The test passes without any errors or warnings, although the docs say:
If there are any pending timers at the end of the function, an exception will be thrown.
So it seems we have a race condition here, i.e. confirm()
is resolved before returning fakeAsync()
but after expect()
. If this is possible, what is the deal about fakeAsync()
, if not controlling those things?
Probably I and other developers will forget tick()
or flushMicrotasks()
in future as well. So I wonder, how to avoid this. Is there some kind of helper function I'm missing that I can put in afterEach()
? Or is the behavior of fakeAsync()
an Angular bug, i.e. it should throw an exception?
EDIT
See a full working example of my problem on Stackblitz: https://stackblitz.com/edit/angular-cnmubr. Notice that you have to click the 'refresh'-button of the inner browser view (next to the editor) if you want to re-run the test or after you changed something. The auto-reload feature will not work and throw errors.
I submitted an issue, like someone suggested in the comments.
Upvotes: 1
Views: 1021
Reputation: 27304
If your tests are at least written the same way, one or the other should fail if a tick
is missing / wrong. It isn't perfect, but you could try moving your test logic into a common function so that you have some degree of confidence that it does what you meant:
function callDelete() {
spyOn(personService, 'delete').and.callThrough();
component.deleteEntry(testPerson);
//tick(); // Missing tick()!
}
it('aborts delete if user declined', fakeAsync(() => {
spyOn(appService, 'confirm').and.returnValue(of(false));
callDelete();
expect(personService.delete).not.toHaveBeenCalled();
}));
it('deletes if user accepted', fakeAsync(() => {
spyOn(appService, 'confirm').and.returnValue(of(true));
callDelete();
expect(personService.delete).toHaveBeenCalled();
}));
Now, your second test will fail (because expect
ran before the await
resolved). By debugging this, you'll either notice the logic error in your code under test, fix that, then find the test error. Or, you'll find and fix the test error, which will cause the first test to fail, and then find the logic error. The only way you can miss it is if you tick
wrong in one test but not the other (or fail to test all conditional branches).
Upvotes: 1
Reputation: 3259
This issue reveals that currently fakeAsync()
doesn't throw an exception for pending micro tasks but only for pending timers. At the moment there seems to be no way to ensure there are no pending micro tasks at the end of the test. However, the developers are going to check if this is a feature.
Upvotes: 0