You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NestJS中配置兼容Jest的ioredis RedisService?

问题:NestJS v9 + ioredis v5 + Jest v29 下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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 08:30:02