Skip to content

Commit f64b818

Browse files
authored
chore(redis): support ioredis-mock for unit testing (#5817)
## Summary Add built-in support for using [ioredis-mock](https://github.com/stipsan/ioredis-mock) in unit tests, and document how to configure it. ## Problem When using `config.redis.Redis = RedisMock` (ioredis-mock) for unit testing, the app hangs on startup because: 1. `ioredis-mock` emits the `ready` event synchronously during construction 2. `@eggjs/redis` plugin attaches the `ready` listener later in `registerBeforeStart` 3. The `ready` event is missed, causing the app to wait forever ## Solution Auto-enable `weakDependent` mode when `config.redis.Redis` is set to a custom class. This skips the blocking `ready` event check, which is safe for mock clients that are immediately ready. ## Changes ### `plugins/redis/src/lib/redis.ts` - Detect custom Redis class and auto-enable weakDependent ### `plugins/redis/src/config/config.default.ts` - Updated `Redis` config JSDoc with ioredis-mock usage example ### `plugins/redis/README.md` - Added "Using ioredis-mock for Unit Tests" section with install, configure, and notes ### Tests - Added `redisapp-mock` fixture with ioredis-mock config - Added 3 test cases: HTTP request, setex/get, del ## Usage ```ts // config/config.unittest.ts import RedisMock from 'ioredis-mock'; export default function () { return { redis: { Redis: RedisMock, client: { host: '127.0.0.1', port: 6379, password: '', db: 0 }, }, }; } ``` Then remove the `redis` service from your CI workflow — no real Redis needed for unit tests. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added comprehensive Redis plugin integration guide with configuration examples for single-client, multi-client, and cluster setups. * Included testing guidance and examples using ioredis-mock for unit tests with weakDependent configuration details. * Added Chinese translations of Redis tutorials and plugin documentation. * **Tests** * Added test suite for ioredis-mock support with setup and validation tests. * **Chores** * Added ioredis-mock as a development dependency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent ae71b0a commit f64b818

15 files changed

Lines changed: 708 additions & 1 deletion

File tree

plugins/redis/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,49 @@ Stop test redis service
277277
docker compose -f docker-compose.yml down
278278
```
279279
280+
## Using ioredis-mock for Unit Tests
281+
282+
You can use [ioredis-mock](https://github.com/stipsan/ioredis-mock) to replace the real Redis client in unit tests. This eliminates the need for a running Redis server during testing, making your CI faster and local development simpler.
283+
284+
### Install
285+
286+
```bash
287+
npm i --save-dev ioredis-mock @types/ioredis-mock
288+
```
289+
290+
### Configure
291+
292+
In your test config (e.g., `config/config.unittest.ts`), override the `Redis` class:
293+
294+
```ts
295+
import RedisMock from 'ioredis-mock';
296+
import type { EggAppInfo, PartialEggConfig } from 'egg';
297+
298+
export default function (_appInfo: EggAppInfo): PartialEggConfig {
299+
return {
300+
redis: {
301+
Redis: RedisMock,
302+
client: {
303+
host: '127.0.0.1',
304+
port: 6379,
305+
password: '',
306+
db: 0,
307+
weakDependent: true,
308+
},
309+
},
310+
};
311+
}
312+
```
313+
314+
> **Important**: You must set `weakDependent: true` when using `ioredis-mock`. Mock clients emit the `ready` event synchronously during construction, before the plugin's listener is attached. Without `weakDependent: true`, the app will hang on startup waiting for a `ready` event that was already emitted.
315+
316+
### Notes
317+
318+
- `ioredis-mock` provides an in-memory Redis implementation that supports most common commands (`get`, `set`, `setex`, `del`, `incr`, `zadd`, `zpopmin`, `zcount`, etc.)
319+
- Each test worker gets an isolated in-memory Redis instance
320+
- For production deployment testing, you should still use a real Redis server
321+
- You can remove `redis` service containers from your CI workflow when using `ioredis-mock` for unit tests
322+
280323
## Questions & Suggestions
281324
282325
Please open an issue [here](https://github.com/eggjs/egg/issues).

plugins/redis/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"@types/node": "catalog:",
6262
"detect-port": "^2.1.0",
6363
"egg": "workspace:*",
64+
"ioredis-mock": "catalog:",
6465
"typescript": "catalog:"
6566
},
6667
"peerDependencies": {

plugins/redis/src/config/config.default.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,28 @@ export interface RedisConfig {
5050
*/
5151
agent: boolean;
5252
/**
53-
* Customize iovalkey version, only set when you needed
53+
* Customize Redis client class. Use this to replace ioredis with a compatible
54+
* alternative, such as iovalkey or ioredis-mock for unit testing.
55+
*
56+
* When using ioredis-mock, you must also set `weakDependent: true` in the
57+
* client config to avoid startup hangs (mock clients emit 'ready' synchronously
58+
* before the plugin's listener is attached).
59+
*
60+
* @example
61+
* ```ts
62+
* // config/config.unittest.ts
63+
* import RedisMock from 'ioredis-mock';
64+
* import type { EggAppInfo, PartialEggConfig } from 'egg';
65+
*
66+
* export default function (_appInfo: EggAppInfo): PartialEggConfig {
67+
* return {
68+
* redis: {
69+
* Redis: RedisMock,
70+
* client: { host: '127.0.0.1', port: 6379, password: '', db: 0, weakDependent: true },
71+
* },
72+
* };
73+
* }
74+
* ```
5475
*
5576
* Default to `undefined`, which means using the built-in ioredis
5677
*/
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = (app) => {
2+
return class HomeController extends app.Controller {
3+
async index() {
4+
const { ctx, app } = this;
5+
await app.redis.set('foo', 'bar');
6+
ctx.body = await app.redis.get('foo');
7+
}
8+
};
9+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
module.exports = function (app) {
4+
app.get('/', 'home.index');
5+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const RedisMock = require('ioredis-mock');
2+
3+
exports.redis = {
4+
Redis: RedisMock,
5+
client: {
6+
host: '127.0.0.1',
7+
port: 6379,
8+
password: '',
9+
db: 0,
10+
weakDependent: true,
11+
},
12+
};
13+
14+
exports.logger = {
15+
coreLogger: {
16+
level: 'INFO',
17+
},
18+
};
19+
20+
exports.keys = 'keys';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
exports.redis = {
2+
enable: true,
3+
package: '@eggjs/redis',
4+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "redisapp-mock"
3+
}

plugins/redis/test/redis.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import assert from 'node:assert/strict';
12
import compile from 'node:child_process';
23
import path from 'node:path';
34

@@ -196,3 +197,32 @@ describe.skipIf(skip)('test/redis.test.ts', () => {
196197
});
197198
});
198199
});
200+
201+
describe('ioredis-mock', () => {
202+
let app: MockApplication;
203+
beforeAll(async () => {
204+
app = mm.app({
205+
baseDir: getFixtures('apps/redisapp-mock'),
206+
});
207+
await app.ready();
208+
});
209+
afterAll(() => app.close());
210+
afterEach(mm.restore);
211+
212+
it('should work with ioredis-mock (no real Redis needed)', () => {
213+
return app.httpRequest().get('/').expect(200).expect('bar');
214+
});
215+
216+
it('should support setex and get', async () => {
217+
await app.redis.setex('test-key', 60, 'test-value');
218+
const val = await app.redis.get('test-key');
219+
assert.equal(val, 'test-value');
220+
});
221+
222+
it('should support del', async () => {
223+
await app.redis.set('del-key', 'val');
224+
await app.redis.del('del-key');
225+
const val = await app.redis.get('del-key');
226+
assert.equal(val, null);
227+
});
228+
});

pnpm-lock.yaml

Lines changed: 72 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)