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.
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;
});
});