typescriptaws-lambdats-jestnode-redisioredis

Mock ioRedis for connection failure


I'm using ioredis npm library in my production code : file: redisManager.ts

import Redis from 'ioredis';

export interface RedisConnection {
    client: Redis | null;
}

let redisConnection: RedisConnection = {
    client : null
};

async function createRedisClientAndWaitForConnection(): Promise<Redis> {
    console.log(`Creating new Redis client REDIS_HOST=localhost REDIS_PORT=6379`);
    const redisClient = new Redis(6379, 'localhost', {});
    console.log('Waiting for Redis connection to be establish');
    let redisPromiseStatus = 'pending';
    return new Promise((resolve, reject) => {
        redisClient.on('connect', () => console.log(`Got 'connect' event from Redis server 6379:localhost`));
        redisClient.on('ready', () => {
            console.log(`Got "ready" event from Redis. Connection established.`);
            redisPromiseStatus = 'fulfilled'; 
            resolve(redisClient);
        });
        redisClient.on('error', (err) => {
            console.error(`Got error event from Redis. Details: ${err.message}`);
            closeRedisConnection(redisClient);
            if (redisPromiseStatus === 'pending') {
               redisPromiseStatus = 'rejected';
               reject(err);
            } else
               throw err;
        });
    });
}

function getStoredRedisClient(): Redis | null {
    return redisConnection.client;
}

function setStoredRedisClient(redisClient: Redis) {
    redisConnection.client = redisClient;
}

function hasStoredValidRedisConnection() {
    console.log(`hasStoredValidRedisConnection : redisConnection status='${redisConnection?.client?.status}'`)
    return redisConnection?.client?.status === "ready";
}

export function closeRedisConnection(redisClient: Redis) {
    console.log('Closing Redis connection');
    redisClient?.quit();
    redisClient?.removeAllListeners();
    redisConnection.client = null;
}

export async function addKey(redisKey: string, value: string, ttl: number) {
    const redisClient = await getRedisClient();
    console.info(`before redisClient.set redisKey='${redisKey}' value='${JSON.stringify(value)}'`);
    return redisClient.set(redisKey, JSON.stringify(value), "PX", ttl);
}

export async function getKey(redisKey: string) {
    const redisClient = await getRedisClient();
    const redisValue = await redisClient.get(redisKey);
    console.log(`getKey key='${redisKey}' value='${JSON.stringify(redisValue)}'`);
    return (redisValue);
}

export async function getRedisClient():Promise<Redis> {
    console.log(`Trying to get redis client...`);
    try {
        if (hasStoredValidRedisConnection()) {
            console.log(`Lambda has valid Redis Connection in redisConnectionStore - going to use existing client`);
            const redisClient = getStoredRedisClient();
            if (!redisClient) {
                throw new Error("Stored Redis client is invalid");
            }
            return redisClient;
        } else {
            console.log('hasValidRedisConnection=False');
            const redisClient = await createRedisClientAndWaitForConnection();
            setStoredRedisClient(redisClient);
            return redisClient;
        }
    } catch (err) {
        console.error(`Get exception on getRedisClient with error: ${(err as Error)?.message}`);
        throw err;
    }
}

file app.ts

    import * as redisManager from "./redisManager";
    export const lambdaHandler = async (event: SQSEvent, context: Context): Promise<SQSBatchResponse> => {
        context.callbackWaitsForEmptyEventLoop = false;
        const batchFailures: SQSBatchResponse = {
            "batchItemFailures": []
        };
        try {
            await Promise.all(event.Records.map(async (sqsRecord: SQSRecord) => {
                try {
                    const settingConfig = JSON.parse(sqsRecord.body) as SettingsConfig;
                    await redisManager.addKey(settingConfig, 333333);
                } catch (e) {
                    const error = e as Error;
                    logger.error(`Lambda failed. on messageId=${sqsRecord.messageId}: ${error.message}`);
                    batchFailures.batchItemFailures.push({itemIdentifier: sqsRecord.messageId})
                }
            }));
            logger.info(SUCCESS_MESSAGE);
        } catch (e) {
            const error = e as Error;
            logger.error(`Lambda execution failed. Got error: ${error.message}`);
            throw error;
        } finally {
            logger.info(JSON.stringify(batchFailures, null, 2));
            return batchFailures;
        }
    };

I want to simulate connection failure while connecting to Redis via jest.

import {beforeAll, beforeEach, describe, expect, it, jest} from '@jest/globals';
import RedisMock from 'ioredis-mock';
import Redis from "ioredis";
jest.mock('ioredis', () => jest.requireActual('ioredis-mock'));
describe('lambdaHandler()', () => {
 it('Redis connection fail ', async () => {
          const result: SQSBatchResponse = await lambdaHandler(sqsTriggerEvent, CONTEXT);
}
}

How do I make redisClient.on('error') to be triggered, in unit test?
Note: In all other tests to simulate get and set from Redis I'm using ioredis-mock and it works perfectly.


Solution

  • finally I have found a solution to my problem.

    import {beforeAll, beforeEach, describe, expect, it, jest} from '@jest/globals';
    
    describe('lambdaHandler()', () => {
     it('Redis connection fail ', async () => {
            jest.mock('ioredis', () => {
                let emitter = new EventEmitter();
                return jest.fn(() => {
                    let ioredisMock = {
                        status: 'not-ready',
                        on: (eventName: string | symbol, callback: (...args: any[]) => void) => {
                            emitter.on(eventName, callback);
                        },
                        quit: () => {
                        }
                    };
                    process.nextTick(() => {
                        emitter.emit('error', new Error("Got 'error' event from Redis."));
                    });
                    return ioredisMock;
                });
            });
    
            const {lambdaHandler} = require('../app');
            await expect(lambdaHandler(sqsTriggerEvent, CONTEXT)).rejects.toThrowError("Got 'error' event from Redis.");
      }
    }
    

    Important: The jest mock must be defined before importing the real Redis library.

    If I would like the connection to succeed I would define the mock to emit connect and ready :

    jest.mock('ioredis', () => {
        let emitter = new EventEmitter();
        return jest.fn(() => {
            let redisMock = {
                status: 'not-ready',
                on: (eventName: string | symbol, callback: (...args: any[]) => void) => {
                    emitter.on(eventName, callback);
                },
                get: (key: string) => {
                    return 'The value of the key';
                },
                set: (key: string, value: string) => {},
                quit: () => {
                    redisMock.status = 'not-ready';
                }
            };
            process.nextTick(() => {
                emitter.emit('connect');
                redisMock.status = 'ready';
                emitter.emit('ready');
            });
            return redisMock;
        });
    });