如何使用`ioredis`设置一个RedisService?

huangapple go评论70阅读模式
英文:

How to set up a RedisService using Redis from `ioredis`?

问题

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

在运行RedisService的单元测试(使用Jest)时,要么出现问题,要么如果我修复它们,然后在启动Nest时出现以下错误:

错误 #1
在启动Nest或运行端到端测试时:

Nest无法解析RedisService的依赖项(?)。请确保索引[0]处的Redis参数在RedisModule上下文中可用。

潜在解决方案:
- RedisModule是否是有效的NestJS模块?
- 如果Redis是提供程序,它是否属于当前RedisModule?
- 如果Redis是从单独的@Module导出的,请确保在RedisModule中导入了该模块。
  @Module({
    imports: [/*包含Redis的模块*/]
  })

上述错误在启动应用程序或运行端到端测试时重现。

这是我的RedisService,其中单元测试正常工作,但是在启动应用程序或运行端到端测试时会出现错误 #1:

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

@Injectable()
export class RedisService implements OnModuleDestroy {
  constructor(private client: Redis) {} // <-- 这可能是“问题”。这种DI在单元测试中正常工作,但是应用程序和端到端测试失败

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

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

但是,我可以轻松通过使我的RedisService不将Redis从'ioredis'注入到构造函数中并在onModuleInit生命周期钩子中实例化来修复它。但是,如果我停止将其注入到构造函数中,那么其单元测试将失败,因为redisClient是空对象,而不是我想要的模拟对象。这导致修复错误 #1,但出现了下面描述的错误 #2。

错误 #2
如果测试失败,我会收到以下类型的错误:

TypeError: Cannot read properties of undefined (reading 'set')
TypeError: Cannot read properties of undefined (reading 'get')

如果我将redis.service.ts更改为以下内容,则单元测试失败,但端到端测试和应用程序成功运行:

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

然后测试失败,因为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在依赖项所在的模块的imports数组中。

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

英文:

NestJs v9.0.0, ioredis v5.3.2, jest v29.5.0.
I'm unable to properly set up my redis service to get it working in both, jest unit tests or starting the nest app. I have a service RedisService which imports Redis from 'ioredis'.

Getting either issues when running the unit tests(jest) for the RedisService, or if I fix them then I get the below error when starting Nest:

Error #1

When starting Nest or running the e2e:

    Nest can&#39;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 */ ]
})

Above error is reproduced when starting the app or running the e2e tests.

This is my RedisService with which the unit tests work fine, but when starting the app or running the e2e tests I get the Error #1:

import { Injectable, OnModuleDestroy } from &#39;@nestjs/common&#39;;
import Redis from &#39;ioredis&#39;;
@Injectable()
export class RedisService implements OnModuleDestroy {
constructor(private client: Redis) {} // &lt;-- This is possibly the &quot;issue&quot;. 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, &#39;EX&#39;, expirationSeconds);
}
async get(key: string): Promise&lt;string | null&gt; {
return await this.client.get(key);
}
}

I have tried different approaches, and this was the one the unit tests finally worked fine, but clearly not when running e2e tests or starting the app.

However I can easily fix it by making my RedisService not to inject Redis from 'ioredis' into the constructor and instead instantiating it in onModuleInit lifecycle hook. BUT if I stop injecting it into the constructor, then its unit tests fail because the redisClient is an empty object instead of the mock I want it to be. Which leads to fix Error #1 but instead get Error #2 described below.

Error #2

In case of the tests failing, I get the following kind of errors:

TypeError: Cannot read properties of undefined (reading &#39;set&#39;)
and TypeError: Cannot read properties of undefined (reading &#39;get&#39;)

The unit tests instead fail BUT the e2e and app work successfully if I change the redis.service.ts to:

import { Injectable, OnModuleDestroy, OnModuleInit } from &#39;@nestjs/common&#39;;
import Redis from &#39;ioredis&#39;;
@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,
});
}
// ...
}

Then the tests fail because the redisService is an empty object.

Context

These are the specs, redis.service.spec.ts:

import { Test, TestingModule } from &#39;@nestjs/testing&#39;;
import Redis from &#39;ioredis&#39;;
import * as redisMock from &#39;redis-mock&#39;;
import { RedisService } from &#39;./redis.service&#39;;
describe(&#39;RedisService&#39;, () =&gt; {
let service: RedisService;
let redisClientMock: redisMock.RedisClient;
beforeEach(async () =&gt; {
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&lt;RedisService&gt;(RedisService);
});
it(&#39;should be defined&#39;, () =&gt; {
expect(service).toBeDefined();
});
describe(&#39;set&#39;, () =&gt; {
it(&#39;should set a value in Redis with expiration date&#39;, async () =&gt; {
const spy = jest.spyOn(redisClientMock, &#39;set&#39;);
await service.set(&#39;my-key&#39;, &#39;my-value&#39;, 60);
expect(spy).toHaveBeenCalledWith(&#39;my-key&#39;, &#39;my-value&#39;, &#39;EX&#39;, 60);
});
});
describe(&#39;get&#39;, () =&gt; {
it(&#39;should return null if the key does not exist&#39;, async () =&gt; {
const spy = jest.spyOn(redisClientMock, &#39;get&#39;).mockReturnValue(undefined);
const value = await service.get(&#39;nonexistent-key&#39;);
expect(value).toBeUndefined();
});
it(&#39;should return the value if the key exists&#39;, async () =&gt; {
jest.spyOn(redisClientMock, &#39;get&#39;).mockReturnValue(&#39;my-value&#39;);
const value = await service.get(&#39;my-key&#39;);
expect(value).toBe(&#39;my-value&#39;);
});
});
});

Here is my
redis.module.ts:

import { Module } from &#39;@nestjs/common&#39;;
import { RedisService } from &#39;./redis.service&#39;;
@Module({
providers: [RedisService],
exports: [RedisService],
})
export class RedisModule {}

RedisModule is in the imports array of the module where it is a dependency.

I guess using ioredis we just have to avoid injecting it in the constructor, but then how can I fix redis.service.spec.ts so that it gets the redisClient on time? Should it be injected as a dependency in the constructor? In any case, how should Redis be implemented in Nest so that both, e2e and unit tests work smoothly?

答案1

得分: 0

I've translated the code parts you provided:

  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',
};
  1. 在模块中提供它,对于我的情况是 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 {}
  1. 在服务中,redis.service.ts,在构造函数中注入它,如下:
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);
  }
}
  1. 最后是测试,redis.service.spec.ts:使用字符串 REDIS_CLIENT 而不是从 ioredis 导入的 Redis。现在看起来像这样:
import { Test, TestingModule } from '@nestjs/testing';
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();
  });
});
英文:

Fixed it after trying different things. Running the unit tests with this command NEST_DEBUG=true npm test helped me narrow down the issues in the end until the unit tests run successfully. Things that fixed it:

  1. Create file redis.provider.ts like this:
    import { Provider } from &#39;@nestjs/common&#39;;
import Redis from &#39;ioredis&#39;;
export type RedisClient = Redis;
export const redisProvider: Provider = {
useFactory: (): RedisClient =&gt; {
return new Redis({
host: &#39;localhost&#39;,
port: 6379,
});
},
provide: &#39;REDIS_CLIENT&#39;,
};
  1. Provide it in the module, in my case, redis.module.ts:
import { Module } from &#39;@nestjs/common&#39;;
import { redisProvider } from &#39;./redis.providers&#39;;
import { RedisService } from &#39;./redis.service&#39;;
@Module({
providers: [redisProvider, RedisService],
exports: [RedisService],
})
export class RedisModule {}
  1. In the service, redis.service.ts, inject it in the constructor like this:
import { Inject, Injectable } from &#39;@nestjs/common&#39;;
import { RedisClient } from &#39;./redis.providers&#39;;
@Injectable()
export class RedisService {
public constructor(
@Inject(&#39;REDIS_CLIENT&#39;)
private readonly client: RedisClient,
) {}
async set(key: string, value: string, expirationSeconds: number) {
await this.client.set(key, value, &#39;EX&#39;, expirationSeconds);
}
async get(key: string): Promise&lt;string | null&gt; {
return await this.client.get(key);
}
}
  1. Finally the test, redis.service.spec.ts: use the string REDIS_CLIENT instead of Redis imported from ioredis. So now it looks like this:
import { Test, TestingModule } from &#39;@nestjs/testing&#39;;
import Redis from &#39;ioredis&#39;;
import * as redisMock from &#39;redis-mock&#39;;
import { RedisService } from &#39;./redis.service&#39;;
describe(&#39;RedisService&#39;, () =&gt; {
let service: RedisService;
let redisClientMock: redisMock.RedisClient;
beforeEach(async () =&gt; {
redisClientMock = {
set: jest.fn(),
get: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
RedisService,
{
provide: &#39;REDIS_CLIENT&#39;,
useValue: redisMock.createClient(),
},
],
}).compile();
redisClientMock = module.get(&#39;REDIS_CLIENT&#39;);
service = module.get&lt;RedisService&gt;(RedisService);
});
it(&#39;should be defined&#39;, () =&gt; {
expect(service).toBeDefined();
});

huangapple
  • 本文由 发表于 2023年5月23日 01:43:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/76308716.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定