I'm writing a Test with Angular TestBed with the following setup:
let cacheService: CacheService;
let store: Store<PsaAppState>;
let service: ConfigService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ConfigService,
{ provide: TranslateService, useValue: getMock(TranslateService) },
{ provide: CacheService, useValue: getMock(CacheService) },
{ provide: Store, useValue: getMock(Store) }
]
});
const injector: TestBed = getTestBed();
service = injector.get(ConfigService);
cacheService = injector.get(CacheService);
store = injector.get(Store);
});
The test looks like this:
it('Should dispatch FetchFeatureConfigAction when promise is rejected', () => {
spyOn(store, 'dispatch').and.stub();
spyOn(cacheService, 'getRawItem').and.returnValue(Promise.reject('error'));
service.getFeatureConfig();
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(new FetchFeatureConfigAction());
});
The code it tests look like this:
getFeatureConfig() {
console.log('###Service called!!!');
this.cacheService.getRawItem('APP_STATE_NAME').then(
(appState: PsaAppState) => {
console.log('###Resolve with ', appState);
!isNil(appState.featureConfigState.featureConfig)
? this.store$.dispatch(new FetchFeatureConfigSuccessAction(appState.featureConfigState.featureConfig))
: this.store$.dispatch(new FetchFeatureConfigAction());
},
err => {
console.log('###Rejected with ', err);
this.store$.dispatch(new FetchFeatureConfigAction());
}
);
}
I can see the logs in the rejected callback (also in the resolved for other tests) but the expects are failing with no interaction. My assumption is that to mock this.store$.dispatch
in a Promis scope is the issue.
Is this assumption correct and how can I make this test run?
The root cause is that the Promise is not resolved yet when the service is called and the console.log
is executed. This is due to the way the Javascript Event Loop is handling asynchronous requests.
The fix to the error case is following:
it('Should dispatch FetchFeatureConfigAction when promise is rejected', () => {
spyOn(store, 'dispatch').and.stub();
const expectedPromise = Promise.reject('error');
spyOn(cacheService, 'getRawItem').and.returnValue(expectedPromise);
service.determineFeatureConfig();
expectedPromise.catch(() => {
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(new FetchFeatureConfigAction());
});
});
I extracted the Promise to a variable and then inside the catch the expects are working fine.
For the good cases the solution can be either with then or async/await syntax. An example for the latter would look like this:
it('Should dispatch FetchFeatureConfigAction when promise is resolved', async () => {
spyOn(store, 'dispatch').and.stub();
const expectedPromise = Promise.resolve('success');
spyOn(cacheService, 'getRawItem').and.returnValue(expectedPromise);
service.determineFeatureConfig();
await expectedPromise;
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(new FetchFeatureConfigAction());
});