typescript - 如何使用 `ioredis`中的Redis设置RedisService?

标签 typescript redis jestjs nestjs ioredis

NestJs v9.0.0、ioredis v5.3.2、jest v29.5.0。 我无法正确设置我的 redis 服务以使其在 Jest 单元测试或启动 Nest 应用程序中工作。我有一个服务 RedisService,它从“ioredis”导入 Redis。

在为 RedisService 运行单元测试( Jest )时遇到问题,或者如果我修复了这些问题,那么在启动 Nest 时会出现以下错误:

错误#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 */ ]
      })

启动应用程序或运行 e2e 测试时会重现上述错误。

这是我的 RedisService,单元测试可以正常工作,但是在启动应用程序或运行 e2e 测试时,我收到错误 #1:

import { Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class RedisService implements OnModuleDestroy {
  constructor(private client: Redis) {} // <-- This is possibly the "issue". Unit tests work fine with this DI but app and e2e fail

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

我尝试了不同的方法,这是单元测试最终工作正常的方法,但在运行 e2e 测试或启动应用程序时显然不行。

但是,我可以通过使我的 RedisService 不将“ioredis”中的 Redis 注入(inject)到构造函数中,而是在 onModuleInit 生命周期 Hook 中实例化它来轻松修复此问题。但是如果我停止将它注入(inject)到构造函数中,那么它的单元测试就会失败,因为 redisClient 是一个空对象,而不是我想要的模拟对象。这会导致修复错误 #1,但会出现如下所述的错误 #2。

错误#2

如果测试失败,我会收到以下错误:

类型错误:无法读取未定义的属性(读取“设置”)类型错误:无法读取未定义的属性(读取“get”)

单元测试会失败,但如果我将 redis.service.ts 更改为:

,e2e 和应用程序会成功工作
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class RedisService implements OnModuleInit, OnModuleDestroy {
  private client: Redis; // no injection in the constructor

  async onModuleInit() {
    this.client = new Redis({
      host: process.env.REDIS_HOST,
      port: +process.env.REDIS_PORT,
    });
  }
  // ...
}

然后测试失败,因为 redisService 是一个空对象。

上下文

这些是规范,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');
    });
  });
});

这是我的 redis.module.ts:

import { Module } from '@nestjs/common';
import { RedisService } from './redis.service';

@Module({
  providers: [RedisService],
  exports: [RedisService],
})
export class RedisModule {}

RedisModule 位于作为依赖项的模块的导入数组中。

我想使用 ioredis 我们只需避免将其注入(inject)到构造函数中,但是如何修复 redis.service.spec.ts 以便它按时获取 redisClient 呢?它应该作为构造函数中的依赖项注入(inject)吗?无论如何,Redis 应该如何在 Nest 中实现,以便 e2e 和单元测试都能顺利进行?

最佳答案

尝试不同的方法后修复了它。使用此命令 NEST_DEBUG=true npm test 运行单元测试帮助我最终缩小了问题范围,直到单元测试成功运行。修复它的事情:

  1. 像这样创建文件redis.provider.ts:
    import { Provider } from '@nestjs/common';
    import Redis from 'ioredis';
    
    export type RedisClient = Redis;
    
    export const redisProvider: Provider = {
      useFactory: (): RedisClient => {
        return new Redis({
          host: 'localhost',
          port: 6379,
        });
      },
      provide: 'REDIS_CLIENT',
    };
  • 在模块中提供它,在我的例子中,redis.module.ts:
  • import { Module } from '@nestjs/common';
    import { redisProvider } from './redis.providers';
    import { RedisService } from './redis.service';
    
    @Module({
      providers: [redisProvider, RedisService],
      exports: [RedisService],
    })
    export class RedisModule {}
    
  • 在服务 redis.service.ts 中,将其注入(inject)到构造函数中,如下所示:
  • import { Inject, Injectable } from '@nestjs/common';
    import { RedisClient } from './redis.providers';
    
    @Injectable()
    export class RedisService {
      public constructor(
        @Inject('REDIS_CLIENT')
        private readonly client: RedisClient,
      ) {}
    
      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);
      }
    }
    
  • 最后是测试,redis.service.spec.ts:使用字符串REDIS_CLIENT,而不是从ioredis导入的Redis。所以现在看起来像这样:
  • 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_CLIENT',
              useValue: redisMock.createClient(),
            },
          ],
        }).compile();
    
        redisClientMock = module.get('REDIS_CLIENT');
        service = module.get<RedisService>(RedisService);
      });
    
      it('should be defined', () => {
        expect(service).toBeDefined();
      });
    

    关于typescript - 如何使用 `ioredis`中的Redis设置RedisService?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76308716/

    相关文章:

    reactjs - React 测试库元素标题未定义

    javascript - node_modules\ng2-dragula\index.js 未导出“DragulaModule”

    debugging - 如何在 Visual Studio Code 中调试 Angular2 TypeScript 应用程序?

    python, redis : store on redis a multi-dimensional list. 最佳数据类型?

    node.js - 我应该在文件/模块之间共享 Redis 连接吗?

    javascript - 在使用 react-test-renderer 的 Jest 快照测试中,Refs 为空

    typescript - 在 Typescript 的函数中模拟获取

    javascript - Npm 以 Angular 启动需要很长时间

    javascript - 实现 JavaScript Web API 接口(interface)

    kubernetes - Bitnami Redis 在 Kubernetes 身份验证失败时使用现有 key