I am testing a service that has methods internally, but functionality to external sources is mocked.
In my example here, I have a method that is doing some processing. Should the validation method fail, it will send an Alert (email, other) to an external service.
I want to test that my code will call the alert when it should, but I obviously do not want it to call the external system.
async Process(DateToProcess: number): Promise<void> {
const IsWeekend: boolean = IsSaturdayOrSunday(DateToProcess);
...
const DataToValidate$: Array<ResponseDTO> = await this.GetData(DateToProcess);
const IsValid$: boolean = await this.ValidateEntries(DataToValidate$);
if (! IsValid$) {
await this.CreateAlert('Data is Invalid');
}
}
I am trying to setup the testing, and have the external services with stubs.
const AlertDate = new ResponseDTO();
const mockAlertService = {
provide: AlertsService,
useFactory: () => ({
Create: jest.fn().mockResolvedValue(AlertData),
Delete: jest.fn().mockResolvedValue(null),
GetOne: jest.fn().mockResolvedValue(AlertData),
})
}
...
const module: TestingModule = await Test.createTestingModule({
providers: [
ValidateService,
mockAlertService,
],
}).compile();
service = module.get<ValidateDailyTimeEntriesService>(ValidateDailyTimeEntriesService);
AlertssTestService = module.get<AlertsService>(AlertsService);
This setup works, I can mock the return values. I have other services included that I have removed here, that use the same format and load basic test data, which I can validate works.
My tests though, cannot validate that the Alert is called to be created.
it('should send an alert if there are no times entries and it is not a weekend', async () => {
const spyProcess_: jest.SpyInstance = jest.spyOn(service, 'Process');
const spyGetData_: jest.SpyInstance = jest.spyOn(service, 'GetData').mockResolvedValue(TestData);
const spyValidateData_: jest.SpyInstance = jest.spyOn(service, 'ValidateData');
const spyCreateAlert_: jest.SpyInstance = jest.spyOn(service, 'CreateAlert').mockResolvedValue(true);
await service.Process(20221012); // Call the main function
expect(spyProcess_).toHaveBeenCalled(); // These 2 calls work, were called in the this test
expect(spyProcess_).toHaveBeenCalledWith(20221012);
expect(spyGetData_).toHaveBeenCalled(); // Works, this was called
expect(spyGetData_).toHaveReturnedWith(TestData);
expect(spyValidateData_).toHaveBeenCalled(); // This fails???
expect(spyCreateAlert_).toHaveBeenCalled(); // This will fail as well
spyCreateAlert_.mockClear();
spyValidateData_.mockClear();
spyGetData_.mockClear();
spyProcessOneDate_.mockClear();
});
The error shows that the last two spies will not work, even though they appear to me to be the same style as the ones that do.
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
> 298 | expect(spyValidateData_).toHaveBeenCalled();
> 298 | expect(spyCreateAlert_).toHaveBeenCalled();
EDIT It has to do with the async/await vs Promise handling. If I do:
async Process(): Promise<void> {
this.GetData(DateToProcess).then( (DataToValidate$: Array<ResponseDTO>) => {});
this.CreateAlert('Data is Invalid').then( (Result$: boolean) => {});
return;
}
The tests will pass. However, using the await will fail:
async Process(): Promise<void> {
const DataToValidate$: Array<ResponseDTO> = await this.GetData(DateToProcess);
const Result$: boolean = await this.CreateAlert();
return;
}
CodePudding user response:
My issue has to do with the processing I am doing. await/async cannot run in loop that have callbacks, such as forEach(). Use the for..of format, will correct the errors I am encountering. It takes some restructing of the code I have written, but it does correct the issue.
My simple example did not show the looping in the secondary functions. Therefore, the simple re-write would be:
const DataToValidate$: Array<ResponseDTO> = await this.GetData(DateToProcess);
DataToValidate$.forEach( (OneRecord: ResponseDTO) => {
const IsValid$: boolean = await this.ValidateEntries(OneRecord);
}
However, VS Code indicates I need an async before the await
const DataToValidate$: Array<ResponseDTO> = await this.GetData(DateToProcess);
DataToValidate$.forEach(async (OneRecord: ResponseDTO) => {
const IsValid$: boolean = await this.ValidateEntries(OneRecord);
}
However, this does not work as the async in the forEach() does not work, since forEach is a callback style loop.
const DataToValidate$: Array<ResponseDTO> = await this.GetData(DateToProcess);
for (OneRecord of DataToValidate$) {
const IsValid$: boolean = await this.ValidateEntries(OneRecord);
}
This will allow the proper processing.