如何在NestJS中配置兼容Jest的ioredis RedisService?
我使用NestJs v9.0.0、ioredis v5.3.2、jest v29.5.0,无法正确配置RedisService以同时兼容Jest单元测试和Nest应用启动。我的RedisService导入了ioredis的Redis类,目前遇到以下冲突问题:
错误#1
启动Nest应用或运行E2E测试时,出现如下错误:
Nest can't resolve dependencies of the RedisService (?). Please make sure that the argument Redis at index [0] is available in the RedisModule context. Potential solutions: - Is RedisModule a valid NestJS module? - If Redis is a provider, is it part of the current RedisModule? - If Redis is exported from a separate @Module, is that module imported within RedisModule? @Module({ imports: [ /* the Module containing Redis */ ] })
此时我的RedisService代码如下,单元测试可正常运行,但应用和E2E测试触发上述错误:
import { Injectable, OnModuleDestroy } from '@nestjs/common'; import Redis from 'ioredis'; @Injectable() export class RedisService implements OnModuleDestroy { constructor(private client: Redis) {} // <-- 此处为问题点,单元测试正常但应用和E2E失败 async onModuleInit() { this.client = new Redis({ host: process.env.REDIS_HOST, port: +process.env.REDIS_PORT, }); } async onModuleDestroy() { await this.client.quit(); } async set(key: string, value: string, expirationSeconds: number) { await this.client.set(key, value, 'EX', expirationSeconds); } async get(key: string): Promise<string | null> { return await this.client.get(key); } }
错误#2
若修改RedisService不在构造函数注入Redis,而是在onModuleInit生命周期钩子中实例化客户端,应用和E2E测试可正常运行,但单元测试出现如下错误:TypeError: Cannot read properties of undefined (reading 'set')
以及TypeError: Cannot read properties of undefined (reading 'get')
修改后的RedisService代码如下:
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import Redis from 'ioredis'; @Injectable() export class RedisService implements OnModuleInit, OnModuleDestroy { private client: Redis; // 构造函数不注入 async onModuleInit() { this.client = new Redis({ host: process.env.REDIS_HOST, port: +process.env.REDIS_PORT, }); } // ... 其余方法与之前一致 }
上下文信息
我的单元测试代码(redis.service.spec.ts)如下:
import { Test, TestingModule } from '@nestjs/testing'; import Redis from 'ioredis'; import * as redisMock from 'redis-mock'; import { RedisService } from './redis.service'; describe('RedisService', () => { let service: RedisService; let redisClientMock: redisMock.RedisClient; beforeEach(async () => { redisClientMock = { set: jest.fn(), get: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ RedisService, { provide: Redis, useValue: redisMock.createClient(), }, ], }).compile(); redisClientMock = module.get(Redis); service = module.get<RedisService>(RedisService); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('set', () => { it('should set a value in Redis with expiration date', async () => { const spy = jest.spyOn(redisClientMock, 'set'); await service.set('my-key', 'my-value', 60); expect(spy).toHaveBeenCalledWith('my-key', 'my-value', 'EX', 60); }); }); describe('get', () => { it('should return null if the key does not exist', async () => { const spy = jest.spyOn(redisClientMock, 'get').mockReturnValue(undefined); const value = await service.get('nonexistent-key'); expect(value).toBeUndefined(); }); it('should return the value if the key exists', async () => { jest.spyOn(redisClientMock, 'get').mockReturnValue('my-value'); const value = await service.get('my-key'); expect(value).toBe('my-value'); }); }); });
我的RedisModule代码如下:
import { Module } from '@nestjs/common'; import { RedisService } from './redis.service'; @Module({ providers: [RedisService], exports: [RedisService], }) export class RedisModule {}
RedisModule已被添加到依赖它的模块的imports数组中。我猜想使用ioredis时应避免在构造函数中注入,但如何修改单元测试代码以确保能正确获取redisClient?或者是否应该在构造函数中注入?无论如何,如何在Nest中实现Redis以同时兼容E2E和单元测试?
方案一:通过依赖注入管理Redis客户端(推荐)
核心思路是在RedisModule中注册Redis客户端作为可注入的provider,统一应用启动、E2E测试、单元测试的客户端获取方式,从根源避免冲突。
1. 修改RedisModule,注册Redis客户端
import { Module } from '@nestjs/common'; import Redis from 'ioredis'; import { RedisService } from './redis.service'; @Module({ providers: [ RedisService, { provide: 'REDIS_CLIENT', // 使用自定义token,避免与ioredis类直接绑定 useFactory: () => { return new Redis({ host: process.env.REDIS_HOST, port: +process.env.REDIS_PORT, }); }, }, ], exports: [RedisService, 'REDIS_CLIENT'], }) export class RedisModule {}
2. 修改RedisService,注入Redis客户端
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common'; import Redis from 'ioredis'; @Injectable() export class RedisService implements OnModuleDestroy { constructor(@Inject('REDIS_CLIENT') private client: Redis) {} async onModuleDestroy() { await this.client.quit(); } async set(key: string, value: string, expirationSeconds: number) { await this.client.set(key, value, 'EX', expirationSeconds); } async get(key: string): Promise<string | null> { return await this.client.get(key); } }
这里移除了onModuleInit,因为客户端已经在Module的useFactory中实例化并注入,无需重复创建。
3. 修改单元测试,模拟Redis客户端
import { Test, TestingModule } from '@nestjs/testing'; import Redis from 'ioredis'; import { RedisService } from './redis.service'; describe('RedisService', () => { let service: RedisService; let redisClientMock: jest.Mocked<Redis>; beforeEach(async () => { // 创建模拟的Redis客户端,确保异步方法返回Promise redisClientMock = { set: jest.fn().mockResolvedValue('OK'), get: jest.fn(), quit: jest.fn().mockResolvedValue('OK'), } as jest.Mocked<Redis>; const module: TestingModule = await Test.createTestingModule({ providers: [ RedisService, { provide: 'REDIS_CLIENT', useValue: redisClientMock, }, ], }).compile(); service = module.get<RedisService>(RedisService); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('set', () => { it('should set a value in Redis with expiration date', async () => { await service.set('my-key', 'my-value', 60); expect(redisClientMock.set).toHaveBeenCalledWith('my-key', 'my-value', 'EX', 60); }); }); describe('get', () => { it('should return null if the key does not exist', async () => { redisClientMock.get.mockResolvedValue(null); const value = await service.get('nonexistent-key'); expect(value).toBeNull(); }); it('should return the value if the key exists', async () => { redisClientMock.get.mockResolvedValue('my-value'); const value = await service.get('my-key'); expect(value).toBe('my-value'); }); }); });
方案二:适配无注入的RedisService(针对错误#2的修复)
如果不想使用依赖注入,只想保留onModuleInit中实例化客户端的方式,需要在单元测试中手动初始化客户端:
import { Test, TestingModule } from '@nestjs/testing'; import Redis from 'ioredis'; import { RedisService } from './redis.service'; describe('RedisService', () => { let service: RedisService; let redisClientMock: jest.Mocked<Redis>; beforeEach(async () => { redisClientMock = { set: jest.fn().mockResolvedValue('OK'), get: jest.fn(), quit: jest.fn().mockResolvedValue('OK'), } as jest.Mocked<Redis>; const module: TestingModule = await Test.createTestingModule({ providers: [RedisService], }).compile(); service = module.get<RedisService>(RedisService); // 直接给service的client属性赋值模拟对象 service['client'] = redisClientMock; }); // 测试用例与方案一一致 });
关键说明
- 方案一遵循NestJS依赖注入原则,更利于扩展维护,同时统一了所有场景的客户端获取逻辑,彻底解决冲突。
- 错误#1的根源是RedisModule未注册
Redis类作为provider,Nest无法找到注入实例;错误#2的根源是单元测试中onModuleInit未触发,导致client未初始化。
内容的提问来源于stack exchange,提问作者Laszlo Sarvold

