Search code examples
javascripttypescriptjestjsts-jestjest-mock-extended

Jest mock is entering function logic instead of returning rejected promise


The Problem

What I'm trying to do is test that the error is caught and returned with the correct status code when a use case function returns a rejected promise.

The problem I'm having is that instead of a rejected promise, i just get this error instead of the mock function just returning a rejected promise.

This is the error that I'm getting: TypeError: Cannot read properties of undefined (reading 'then')

The code

Inside a test suite I have this setup:

const container = new Container();

describe('Controller Error Tests', () => {
     let reportController: ReportController;
     
     let generateReportUseCase: IGenerateReportUseCase = mock<IGenerateReportUseCase>();

     container.bind<ReportServiceLocator>(TYPES.ReportServiceLocator).to(ReportServiceLocator);
     
     beforeAll(async () => {
        jest.clearAllMocks();
        cleanUpMetadata();
        dotenv.config();
        reportController = new ReportController(container.get<ReportServiceLocator>(Symbol.for("ReportServiceLocator")));
    });

     it('User held shares report use case throws error', async () => {
        let requestObj = httpMocks.createRequest({
            cookies: {
                token: jwt.sign({ id: 'test' }, process.env.JWT_SECRET_KEY!),
            },
            query: {
                report_type: 'CSV'
            }
        });

        let responseObj = httpMocks.createResponse();

        mock(generateReportUseCase).usersHeldShares.mockRejectedValue(new Error('Could not generate report'));

        await reportController.userHeldShares(requestObj, responseObj);
        expect(responseObj.statusCode).toEqual(500);
    })
})

reportController.userHeldShares is an inversify controller that looks like this:

@httpGet('/held-shares')
    public async userHeldShares(@request() req: express.Request, @response() res: express.Response){
        let jwtSecretKey = process.env.JWT_SECRET_KEY;
        let ascending: boolean = req.query.ascending === "false" ? false : true;
        let report_type: string = String(req.query.reportformat);
        let cookieData = await <IUserDto>jwt.verify(req.cookies.token, jwtSecretKey!);
            
        if(!cookieData.id){
            return res.status(401).json({error: 'User not authorised'});
        }
        
        return await this.generateReportUseCase.usersHeldShares(cookieData.id!, ascending, report_type)
            .then((userDto: IUserDto) => {
                res.status(200).json(userDto)
            })
            .catch((err: Error) => {
                res.status(500).json(err);
            });
    }

Here is what the first few lines of generateReportUseCase.usersHeldShares looks like, which is where the error comes from:

usersHeldShares(user_id: string, ascending: boolean, report_type: string): Promise<IUserDto> {
        return new Promise(async (resolve, reject) => {
            this.tradeReadOnlyRepository.fetch({user_id: user_id}, false)
            .then(async trades => {

What I am expecting

When it hits the line return await this.generateReportUseCase.usersHeldShares(cookieData.id!, ascending, report_type) in the inversify controller it just returns a rejected promise without entering the logic of the actual function.


Solution

  • It turns out I was still using a service locator for the proper implementation of the report generator. I replaced this with a TestServiceLocator which will now contain all of the mock return values.

    The implementation of this is like so: container.bind<TestServiceLocator>(TYPES.ReportServiceLocator).to(TestServiceLocator);

    @injectable()
    export default class TestServiceLocator {
        constructor(@inject(TYPES.IStockReadOnlyRepository) private stockReadRepository: IStockReadOnlyRepository,
                    @inject(TYPES.IStockWriteOnlyRepository) private stockWriteRepository: IStockWriteOnlyRepository,
                    @inject(TYPES.IUserWriteOnlyRepository) private userWriteRepository: IUserWriteOnlyRepository,
                    @inject(TYPES.IUserReadOnlyRepository) private userReadRepository: IUserReadOnlyRepository,
                    @inject(TYPES.ITradeReadOnlyRepository) private tradeReadRepository: ITradeReadOnlyRepository,
                    @inject(TYPES.ITradeWriteOnlyRepository) private tradeWriteRepository: ITradeWriteOnlyRepository){}
    
        public GetGenerateReportUseCase(): IGenerateReportUseCase {
            let generateReportUseCase: IGenerateReportUseCase = mock<IGenerateReportUseCase>();
            mock(generateReportUseCase).completeStockValues.mockRejectedValue(new Error('Could not generate report'));
            mock(generateReportUseCase).selectedCompanyDetails.mockRejectedValue(new Error('Could not generate report'));
            mock(generateReportUseCase).usersHeldShares.mockRejectedValue(new Error('Could not generate report'));
            return generateReportUseCase;
        }
    
        public GetDownloadReportUseCase(): IDownloadReportUseCase {
            let downloadReportUseCase: IDownloadReportUseCase = mock<IDownloadReportUseCase>();
            mock(downloadReportUseCase).invoke.mockRejectedValue(new Error('Could not get report data'));
            return downloadReportUseCase;
        }
    }