redis-test-server
v0.0.3
Published
A lightweight, in-memory, Redis-compatible server for cheap, isolated automated tests.
Maintainers
Readme
redis-test-server
A lightweight, in-memory, Redis-compatible server for Node.js, built for one
job: making automated tests cheap and isolated. It speaks the real Redis
protocol (RESP2 and RESP3), so ordinary Redis clients — in particular the
current node-redis — connect and work
without any special adapter.
It is not a production Redis. There is no persistence, replication, clustering, or eviction. It reproduces the Redis interface and the observable semantics your tests depend on, and nothing more.
Why
Spinning up a real Redis (or a container) for every test file is slow and awkward
to isolate. redis-test-server starts in well under a millisecond, listens on an
ephemeral port, and tears down with no leaked timers or handles — so you can run
many completely independent instances in parallel.
Install
npm install --save-dev redis-test-serverNode.js 20+ is required. There are no runtime dependencies.
Usage
import { RedisTestServer } from 'redis-test-server';
import { createClient } from 'redis';
const server = await RedisTestServer.create(); // ephemeral port by default
console.log(server.url); // redis://127.0.0.1:<port>
const client = createClient({ url: server.url });
await client.connect();
await client.set('hello', 'world');
console.log(await client.get('hello')); // 'world'
await client.quit();
await server.close(); // closes sockets, clears dataRedisTestServer.create() accepts:
| Option | Default | Meaning |
| --- | --- | --- |
| port | 0 | 0 lets the OS pick a free port; read it back from server.port. |
| host | 127.0.0.1 | Bind address. |
| databases | 16 | Number of logical databases (as in real Redis). |
| now | Date.now | Injectable clock (ms) — handy for deterministic TTL tests. |
| activeExpiration | false | Enable a background expiry sweep. Off by default so an idle server keeps no timers alive. |
Test isolation
The server is cheap enough that the simplest strategy — one server per test file — is usually the right one:
import { test, before, after } from 'node:test';
import { createClient } from 'redis';
import { RedisTestServer } from 'redis-test-server';
let server, client;
before(async () => {
server = await RedisTestServer.create();
client = createClient({ url: server.url });
await client.connect();
});
after(async () => {
await client.quit();
await server.close();
});If you want to share one TCP server across many tests, each test can use its own
logical database via SELECT, or call FLUSHDB between tests. All of these keep
standard Redis behavior; nothing nonstandard is added to normal commands.
Supported commands
See docs/compatibility.md for the full matrix. Current
coverage:
- Connection: HELLO, PING, ECHO, QUIT, SELECT, CLIENT (SETNAME/GETNAME/SETINFO/ID), COMMAND (stub), DBSIZE, FLUSHDB, FLUSHALL
- Keys: DEL, UNLINK, EXISTS, TYPE, EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT, TTL, PTTL, PERSIST
- Strings: GET, GETEX, SET (EX/PX/EXAT/PXAT/NX/XX/GET/KEEPTTL), SETNX, GETSET, MGET, MSET, INCR, INCRBY, DECR, DECRBY, APPEND, STRLEN
- Hashes: HSET, HGET, HMGET, HGETALL, HDEL, HEXISTS, HLEN, HKEYS, HVALS, HINCRBY
- Sets: SADD, SREM, SISMEMBER, SMEMBERS, SCARD
- Lists: LPUSH, RPUSH, LPUSHX, RPUSHX, LPOP, RPOP, LLEN, LINDEX, LRANGE
- Transactions: MULTI, EXEC, DISCARD, WATCH, UNWATCH
Unknown commands return a standard Redis unknown command error.
Architecture
TCP socket (node:net)
→ streaming RESP decoder (src/resp/decoder.js)
→ command dispatcher (src/commands/registry.js)
→ command implementations (src/commands/*.js)
→ in-memory database (src/database.js, src/value.js)
→ protocol-aware encoder (src/resp/encoder.js)
→ TCP socketProtocol handling, storage, command semantics, and server lifecycle are kept
separate. Storage uses plain Map/Array/Buffer; keys and values are
binary-safe. Expiration is lazy (checked on access) with an optional single
background sweep — never one timer per key.
How compatibility is verified
Redis itself is the behavioral oracle. Two test tiers:
- Fast suite (
npm test) — protocol (RESP2 + RESP3 over a raw socket), command, integration, and lifecycle tests. The integration tests use the currentnode-redis(v6) client over both RESP2 and RESP3. Runs in well under a second and needs no Docker or real Redis. - Differential suite (
npm run test:compat) — launches an officialredis:7.2container and runs identical command scenarios against both real Redis and this server, asserting the results (including error messages, null behavior, TTL semantics, and wrong-type errors) match exactly. Requires Docker; skips gracefully when it is unavailable.
The reference Redis source (tag 7.2.16) is the authority for observable
behavior. It is not committed here; fetch it on demand with npm run
fetch:redis (the differential suite does this automatically). See
docs/redis-source-notes.md for how specific
Redis source functions map to this implementation.
Benchmarks
npm run bench measures the use case that matters: create/destroy cost,
concurrent creation, 1,000 logical databases, memory per database, throughput,
and an empirical comparison of isolation strategies. On a typical dev machine a
create+destroy cycle is ~1 ms and an empty logical database costs ~400 bytes.
See docs/benchmarks.md for numbers and the
strategy recommendation.
Scope
Deliberately out of scope: RDB/AOF persistence, replication, Redis Cluster, Sentinel, ACLs, modules, Lua/EVAL scripting, Streams, Pub/Sub, blocking commands, and eviction policies. The core is structured so more commands can be added without reworking it.
License
MIT for this project. The Redis reference source (fetched on demand into
reference/redis/, not distributed here) is licensed under its own terms
(BSD 3-Clause); see reference/redis/COPYING after fetching, or
THIRD_PARTY_NOTICES.md.
