From 1038321f9a936f029b7c964bf3728d98bd213217 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 12 Oct 2026 19:51:26 +0100 Subject: [PATCH 1/5] Test Valkey, Redis, NATS, etcd and CouchDB against their oldest supported versions --- .../couchdb/src/couchdb-container.test.ts | 7 +- .../modules/etcd/src/etcd-container.test.ts | 24 ++++- .../modules/nats/src/nats-container.test.ts | 13 +-- .../modules/redis/src/redis-container.test.ts | 98 +++++++++++-------- .../valkey/src/valkey-container.test.ts | 34 +++++-- 5 files changed, 116 insertions(+), 60 deletions(-) diff --git a/packages/modules/couchdb/src/couchdb-container.test.ts b/packages/modules/couchdb/src/couchdb-container.test.ts index 2a46976d0..e49f47ab6 100644 --- a/packages/modules/couchdb/src/couchdb-container.test.ts +++ b/packages/modules/couchdb/src/couchdb-container.test.ts @@ -4,16 +4,17 @@ import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { CouchDBContainer } from "./couchdb-container"; const IMAGE = getImage(__dirname); +const OLDEST_IMAGE = "couchdb:3.4.3"; type User = { username: string; email: string; }; -describe("CouchDBContainer", { timeout: 240_000 }, () => { +describe.each([IMAGE, OLDEST_IMAGE])("CouchDBContainer (%s)", { timeout: 240_000 }, (image) => { it("should write and read a collection", async () => { // startContainer { - await using container = await new CouchDBContainer(IMAGE).start(); + await using container = await new CouchDBContainer(image).start(); const client = nano({ url: container.getUrl(), @@ -37,7 +38,7 @@ describe("CouchDBContainer", { timeout: 240_000 }, () => { it("should use custom credentials", async () => { // customCredentials { - await using container = await new CouchDBContainer(IMAGE).withUsername("admin").withPassword("foo").start(); + await using container = await new CouchDBContainer(image).withUsername("admin").withPassword("foo").start(); const client = nano({ url: container.getUrl(), diff --git a/packages/modules/etcd/src/etcd-container.test.ts b/packages/modules/etcd/src/etcd-container.test.ts index dd28a7ce5..bfa70d0fe 100644 --- a/packages/modules/etcd/src/etcd-container.test.ts +++ b/packages/modules/etcd/src/etcd-container.test.ts @@ -3,11 +3,12 @@ import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { EtcdContainer } from "./etcd-container"; const IMAGE = getImage(__dirname); +const OLDEST_IMAGE = "quay.io/coreos/etcd:v3.5.34"; -describe("EtcdContainer", { timeout: 180_000 }, () => { +describe.each([IMAGE, OLDEST_IMAGE])("EtcdContainer (%s)", { timeout: 180_000 }, (image) => { it("should connect and perform read/write operations", async () => { // readWrite { - await using container = await new EtcdContainer(IMAGE).start(); + await using container = await new EtcdContainer(image).start(); const client = new Etcd3({ hosts: container.getClientEndpoint(), @@ -24,7 +25,7 @@ describe("EtcdContainer", { timeout: 180_000 }, () => { it("should subscribe to key changes", async () => { // etcdSubscribe { - await using container = await new EtcdContainer(IMAGE).start(); + await using container = await new EtcdContainer(image).start(); const client = new Etcd3({ hosts: container.getClientEndpoint(), @@ -42,3 +43,20 @@ describe("EtcdContainer", { timeout: 180_000 }, () => { // } }); }); + +describe("EtcdContainer", { timeout: 180_000 }, () => { + it.each(["quay.io/coreos/etcd:v3.6.15"])( + "should connect and perform read/write operations with %s", + async (image) => { + await using container = await new EtcdContainer(image).start(); + + const client = new Etcd3({ + hosts: container.getClientEndpoint(), + }); + + await client.put("foo").value("bar"); + + expect(await client.get("foo").string()).toEqual("bar"); + } + ); +}); diff --git a/packages/modules/nats/src/nats-container.test.ts b/packages/modules/nats/src/nats-container.test.ts index 39800661c..d5ef23449 100644 --- a/packages/modules/nats/src/nats-container.test.ts +++ b/packages/modules/nats/src/nats-container.test.ts @@ -4,8 +4,9 @@ import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { NatsContainer } from "./nats-container"; const IMAGE = getImage(__dirname); +const OLDEST_IMAGE = "nats:2.14.7-alpine"; -describe("NatsContainer", { timeout: 180_000 }, () => { +describe.each([IMAGE, OLDEST_IMAGE])("NatsContainer (%s)", { timeout: 180_000 }, (image) => { it("should subscribe and receive one published message", async () => { // natsPubsub { const SUBJECT = "HELLO"; @@ -13,7 +14,7 @@ describe("NatsContainer", { timeout: 180_000 }, () => { const TE = new TextEncoder(); const TD = new TextDecoder(); - await using container = await new NatsContainer(IMAGE).start(); + await using container = await new NatsContainer(image).start(); const nc = await connect(container.getConnectionOptions()); const sub = nc.subscribe(SUBJECT); @@ -36,7 +37,7 @@ describe("NatsContainer", { timeout: 180_000 }, () => { it("should start with alternative username and password ", async () => { // natsCredentials { - await using container = await new NatsContainer(IMAGE).withUsername("George").withPass("1234").start(); + await using container = await new NatsContainer(image).withUsername("George").withPass("1234").start(); // } const nc = await connect(container.getConnectionOptions()); @@ -48,7 +49,7 @@ describe("NatsContainer", { timeout: 180_000 }, () => { it("should start with JetStream ", async () => { // natsJetstream { - await using container = await new NatsContainer(IMAGE).withJetStream().start(); + await using container = await new NatsContainer(image).withJetStream().start(); // } const nc = await connect(container.getConnectionOptions()); @@ -60,7 +61,7 @@ describe("NatsContainer", { timeout: 180_000 }, () => { }); it("should fail without JetStream ", async () => { - await using container = await new NatsContainer(IMAGE).start(); + await using container = await new NatsContainer(image).start(); const nc = await connect(container.getConnectionOptions()); @@ -79,7 +80,7 @@ describe("NatsContainer", { timeout: 180_000 }, () => { // for the complete list of available arguments see: // See Command Line Options section inside [NATS docker image documentation](https://hub.docker.com/_/nats) async function outputVersionAndExit() { - await using container = await new NatsContainer(IMAGE).withArg("version", "").start(); + await using container = await new NatsContainer(image).withArg("version", "").start(); await connect(container.getConnectionOptions()); } diff --git a/packages/modules/redis/src/redis-container.test.ts b/packages/modules/redis/src/redis-container.test.ts index 640e523ea..78c8fc412 100644 --- a/packages/modules/redis/src/redis-container.test.ts +++ b/packages/modules/redis/src/redis-container.test.ts @@ -5,12 +5,13 @@ import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { RedisContainer } from "./redis-container"; const IMAGE = getImage(__dirname); +const OLDEST_IMAGE = "redis:6.2.24"; const REDISSTACK_IMAGE = getImage(__dirname, 1); -describe("RedisContainer", { timeout: 240_000 }, () => { +describe.each([IMAGE, OLDEST_IMAGE])("RedisContainer (%s)", { timeout: 240_000 }, (image) => { it("should connect and execute set-get", async () => { // redisStartContainer { - await using container = await new RedisContainer(IMAGE).start(); + await using container = await new RedisContainer(image).start(); const client = createClient({ url: container.getConnectionUrl() }); await client.connect(); @@ -23,7 +24,7 @@ describe("RedisContainer", { timeout: 240_000 }, () => { }); it("should connect with password and execute set-get", async () => { - await using container = await new RedisContainer(IMAGE).withPassword("test").start(); + await using container = await new RedisContainer(image).withPassword("test").start(); const client = createClient({ url: container.getConnectionUrl() }); await client.connect(); @@ -38,7 +39,7 @@ describe("RedisContainer", { timeout: 240_000 }, () => { // persistentData { const sourcePath = fs.mkdtempSync("redis-"); - await using container = await new RedisContainer(IMAGE).withPassword("test").withPersistence(sourcePath).start(); + await using container = await new RedisContainer(image).withPassword("test").withPersistence(sourcePath).start(); let client = createClient({ url: container.getConnectionUrl() }); await client.connect(); @@ -58,7 +59,7 @@ describe("RedisContainer", { timeout: 240_000 }, () => { it("should load initial data and can read it", async () => { // withPredefinedData { - await using container = await new RedisContainer(IMAGE) + await using container = await new RedisContainer(image) .withPassword("test") .withInitialData(path.join(__dirname, "initData.redis")) .start(); @@ -78,7 +79,7 @@ describe("RedisContainer", { timeout: 240_000 }, () => { }); it("should load initial data once", async () => { - await using container = await new RedisContainer(IMAGE) + await using container = await new RedisContainer(image) .withInitialData(path.join(__dirname, "initCount.redis")) .start(); @@ -94,7 +95,7 @@ describe("RedisContainer", { timeout: 240_000 }, () => { // redisStartWithCredentials { const password = "testPassword"; - await using container = await new RedisContainer(IMAGE).withPassword(password).start(); + await using container = await new RedisContainer(image).withPassword(password).start(); expect(container.getConnectionUrl()).toEqual(`redis://:${password}@${container.getHost()}:${container.getPort()}`); // } @@ -110,7 +111,7 @@ describe("RedisContainer", { timeout: 240_000 }, () => { it("should execute container cmd and return the result", async () => { // executeCommand { - await using container = await new RedisContainer(IMAGE).start(); + await using container = await new RedisContainer(image).start(); const queryResult = await container.executeCliCmd("info", ["clients"]); @@ -118,23 +119,8 @@ describe("RedisContainer", { timeout: 240_000 }, () => { // } }); - it("should start with redis-stack-server and json module", async () => { - // startWithRedisStack { - await using container = await new RedisContainer(REDISSTACK_IMAGE).withPassword("testPassword").start(); - - const client = createClient({ url: container.getConnectionUrl() }); - await client.connect(); - - await client.json.set("key", "$", { name: "test" }); - const result = await client.json.get("key"); - expect(result).toEqual({ name: "test" }); - - client.destroy(); - // } - }); - it("should start redis with custom command", async () => { - const container = new RedisContainer(IMAGE).withCommand(["redis-server", "--loglevel", "verbose"]); + const container = new RedisContainer(image).withCommand(["redis-server", "--loglevel", "verbose"]); await using startedContainer = await container.start(); // @ts-expect-error - accessing private property for testing @@ -149,12 +135,22 @@ describe("RedisContainer", { timeout: 240_000 }, () => { client.destroy(); }); - it("should start redis-stack with custom env", async () => { - const container = new RedisContainer(REDISSTACK_IMAGE).withEnvironment({ REDIS_ARGS: "--loglevel verbose" }); + it("should start redis with custom command and keeping default args", async () => { + const sourcePath = fs.mkdtempSync("redis-"); + + const container = new RedisContainer(image) + .withCommand(["redis-server", "--loglevel", "verbose"]) + .withPersistence(sourcePath); await using startedContainer = await container.start(); // @ts-expect-error - accessing private property for testing - expect(container.createOpts.Env).toEqual(["REDIS_ARGS=--loglevel verbose"]); + expect(container.createOpts.Cmd).toEqual([ + "redis-server", + "--loglevel", + "verbose", + "--save 1 1 ", + "--appendonly yes", + ]); const client = createClient({ url: startedContainer.getConnectionUrl() }); await client.connect(); @@ -163,24 +159,47 @@ describe("RedisContainer", { timeout: 240_000 }, () => { expect(await client.get("key")).toBe("val"); client.destroy(); + fs.rmSync(sourcePath, { force: true, recursive: true }); }); +}); - it("should start redis with custom command and keeping default args", async () => { - const sourcePath = fs.mkdtempSync("redis-"); +describe("RedisContainer", { timeout: 240_000 }, () => { + it.each(["redis:7.2.16", "redis:7.4.11", "redis:8.2.10"])( + "should connect and execute set-get with %s", + async (image) => { + await using container = await new RedisContainer(image).start(); - const container = new RedisContainer(IMAGE) - .withCommand(["redis-server", "--loglevel", "verbose"]) - .withPersistence(sourcePath); + const client = createClient({ url: container.getConnectionUrl() }); + await client.connect(); + + await client.set("key", "val"); + expect(await client.get("key")).toBe("val"); + + client.destroy(); + } + ); + + it("should start with redis-stack-server and json module", async () => { + // startWithRedisStack { + await using container = await new RedisContainer(REDISSTACK_IMAGE).withPassword("testPassword").start(); + + const client = createClient({ url: container.getConnectionUrl() }); + await client.connect(); + + await client.json.set("key", "$", { name: "test" }); + const result = await client.json.get("key"); + expect(result).toEqual({ name: "test" }); + + client.destroy(); + // } + }); + + it("should start redis-stack with custom env", async () => { + const container = new RedisContainer(REDISSTACK_IMAGE).withEnvironment({ REDIS_ARGS: "--loglevel verbose" }); await using startedContainer = await container.start(); // @ts-expect-error - accessing private property for testing - expect(container.createOpts.Cmd).toEqual([ - "redis-server", - "--loglevel", - "verbose", - "--save 1 1 ", - "--appendonly yes", - ]); + expect(container.createOpts.Env).toEqual(["REDIS_ARGS=--loglevel verbose"]); const client = createClient({ url: startedContainer.getConnectionUrl() }); await client.connect(); @@ -189,7 +208,6 @@ describe("RedisContainer", { timeout: 240_000 }, () => { expect(await client.get("key")).toBe("val"); client.destroy(); - fs.rmSync(sourcePath, { force: true, recursive: true }); }); it("should start redis-stack with custom env and keeping default args", async () => { diff --git a/packages/modules/valkey/src/valkey-container.test.ts b/packages/modules/valkey/src/valkey-container.test.ts index ff82f5fa2..9fd58206a 100644 --- a/packages/modules/valkey/src/valkey-container.test.ts +++ b/packages/modules/valkey/src/valkey-container.test.ts @@ -5,11 +5,12 @@ import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { ValkeyContainer } from "./valkey-container"; const IMAGE = getImage(__dirname); +const OLDEST_IMAGE = "valkey/valkey:7.2.14"; -describe("ValkeyContainer", { timeout: 240_000 }, () => { +describe.each([IMAGE, OLDEST_IMAGE])("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { it("should connect and execute set-get", async () => { // valkeyStartContainer { - await using container = await new ValkeyContainer(IMAGE).start(); + await using container = await new ValkeyContainer(image).start(); const client = createClient({ url: container.getConnectionUrl() }); await client.connect(); @@ -22,7 +23,7 @@ describe("ValkeyContainer", { timeout: 240_000 }, () => { }); it("should connect with password and execute set-get", async () => { - await using container = await new ValkeyContainer(IMAGE).withPassword("test").start(); + await using container = await new ValkeyContainer(image).withPassword("test").start(); const client = createClient({ url: container.getConnectionUrl() }); await client.connect(); @@ -37,7 +38,7 @@ describe("ValkeyContainer", { timeout: 240_000 }, () => { // valkeyWithPersistentData { const sourcePath = fs.mkdtempSync("valkey-"); - await using container = await new ValkeyContainer(IMAGE).withPassword("test").withPersistence(sourcePath).start(); + await using container = await new ValkeyContainer(image).withPassword("test").withPersistence(sourcePath).start(); let client = createClient({ url: container.getConnectionUrl() }); await client.connect(); @@ -58,7 +59,7 @@ describe("ValkeyContainer", { timeout: 240_000 }, () => { it("should load initial data and can read it", async () => { // valkeyWithPredefinedData { - await using container = await new ValkeyContainer(IMAGE) + await using container = await new ValkeyContainer(image) .withPassword("test") .withInitialData(path.join(__dirname, "initData.valkey")) .start(); @@ -82,7 +83,7 @@ describe("ValkeyContainer", { timeout: 240_000 }, () => { const username = "testUser"; const password = "testPassword"; - await using container = await new ValkeyContainer(IMAGE).withUsername(username).withPassword(password).start(); + await using container = await new ValkeyContainer(image).withUsername(username).withPassword(password).start(); expect(container.getConnectionUrl()).toEqual( `redis://${username}:${password}@${container.getHost()}:${container.getPort()}` @@ -102,7 +103,7 @@ describe("ValkeyContainer", { timeout: 240_000 }, () => { // valkeyWithPassword { const password = "testPassword"; - await using container = await new ValkeyContainer(IMAGE).withPassword(password).start(); + await using container = await new ValkeyContainer(image).withPassword(password).start(); expect(container.getConnectionUrl()).toEqual(`redis://:${password}@${container.getHost()}:${container.getPort()}`); // } @@ -118,7 +119,7 @@ describe("ValkeyContainer", { timeout: 240_000 }, () => { it("should execute container cmd and return the result", async () => { // valkeyExecuteCommand { - await using container = await new ValkeyContainer(IMAGE).start(); + await using container = await new ValkeyContainer(image).start(); const queryResult = await container.executeCliCmd("info", ["clients"]); @@ -126,3 +127,20 @@ describe("ValkeyContainer", { timeout: 240_000 }, () => { // } }); }); + +describe("ValkeyContainer", { timeout: 240_000 }, () => { + it.each(["valkey/valkey:8.0.11", "valkey/valkey:8.1.10", "valkey/valkey:9.0.6"])( + "should connect and execute set-get with %s", + async (image) => { + await using container = await new ValkeyContainer(image).start(); + + const client = createClient({ url: container.getConnectionUrl() }); + await client.connect(); + + await client.set("key", "val"); + expect(await client.get("key")).toBe("val"); + + client.destroy(); + } + ); +}); From 4416c14958d92f19451c093093003b4d5ecee59a Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 10:35:28 +0100 Subject: [PATCH 2/5] Loop the suites over a list of every supported image --- .../couchdb/src/couchdb-container.test.ts | 5 ++-- .../modules/etcd/src/etcd-container.test.ts | 22 ++------------ .../modules/nats/src/nats-container.test.ts | 5 ++-- .../modules/redis/src/redis-container.test.ts | 20 ++----------- .../valkey/src/valkey-container.test.ts | 30 ++++++------------- 5 files changed, 17 insertions(+), 65 deletions(-) diff --git a/packages/modules/couchdb/src/couchdb-container.test.ts b/packages/modules/couchdb/src/couchdb-container.test.ts index e49f47ab6..8a4ea8a16 100644 --- a/packages/modules/couchdb/src/couchdb-container.test.ts +++ b/packages/modules/couchdb/src/couchdb-container.test.ts @@ -3,15 +3,14 @@ import { expect } from "vitest"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { CouchDBContainer } from "./couchdb-container"; -const IMAGE = getImage(__dirname); -const OLDEST_IMAGE = "couchdb:3.4.3"; +const IMAGES = ["couchdb:3.4.3", getImage(__dirname)]; type User = { username: string; email: string; }; -describe.each([IMAGE, OLDEST_IMAGE])("CouchDBContainer (%s)", { timeout: 240_000 }, (image) => { +describe.each(IMAGES)("CouchDBContainer (%s)", { timeout: 240_000 }, (image) => { it("should write and read a collection", async () => { // startContainer { await using container = await new CouchDBContainer(image).start(); diff --git a/packages/modules/etcd/src/etcd-container.test.ts b/packages/modules/etcd/src/etcd-container.test.ts index bfa70d0fe..84644548b 100644 --- a/packages/modules/etcd/src/etcd-container.test.ts +++ b/packages/modules/etcd/src/etcd-container.test.ts @@ -2,10 +2,9 @@ import { Etcd3 } from "etcd3"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { EtcdContainer } from "./etcd-container"; -const IMAGE = getImage(__dirname); -const OLDEST_IMAGE = "quay.io/coreos/etcd:v3.5.34"; +const IMAGES = ["quay.io/coreos/etcd:v3.5.34", "quay.io/coreos/etcd:v3.6.15", getImage(__dirname)]; -describe.each([IMAGE, OLDEST_IMAGE])("EtcdContainer (%s)", { timeout: 180_000 }, (image) => { +describe.each(IMAGES)("EtcdContainer (%s)", { timeout: 180_000 }, (image) => { it("should connect and perform read/write operations", async () => { // readWrite { await using container = await new EtcdContainer(image).start(); @@ -43,20 +42,3 @@ describe.each([IMAGE, OLDEST_IMAGE])("EtcdContainer (%s)", { timeout: 180_000 }, // } }); }); - -describe("EtcdContainer", { timeout: 180_000 }, () => { - it.each(["quay.io/coreos/etcd:v3.6.15"])( - "should connect and perform read/write operations with %s", - async (image) => { - await using container = await new EtcdContainer(image).start(); - - const client = new Etcd3({ - hosts: container.getClientEndpoint(), - }); - - await client.put("foo").value("bar"); - - expect(await client.get("foo").string()).toEqual("bar"); - } - ); -}); diff --git a/packages/modules/nats/src/nats-container.test.ts b/packages/modules/nats/src/nats-container.test.ts index d5ef23449..db465a759 100644 --- a/packages/modules/nats/src/nats-container.test.ts +++ b/packages/modules/nats/src/nats-container.test.ts @@ -3,10 +3,9 @@ import { connect } from "@nats-io/transport-node"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { NatsContainer } from "./nats-container"; -const IMAGE = getImage(__dirname); -const OLDEST_IMAGE = "nats:2.14.7-alpine"; +const IMAGES = ["nats:2.14.7-alpine", getImage(__dirname)]; -describe.each([IMAGE, OLDEST_IMAGE])("NatsContainer (%s)", { timeout: 180_000 }, (image) => { +describe.each(IMAGES)("NatsContainer (%s)", { timeout: 180_000 }, (image) => { it("should subscribe and receive one published message", async () => { // natsPubsub { const SUBJECT = "HELLO"; diff --git a/packages/modules/redis/src/redis-container.test.ts b/packages/modules/redis/src/redis-container.test.ts index 78c8fc412..19650494a 100644 --- a/packages/modules/redis/src/redis-container.test.ts +++ b/packages/modules/redis/src/redis-container.test.ts @@ -4,11 +4,10 @@ import { createClient } from "redis"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { RedisContainer } from "./redis-container"; -const IMAGE = getImage(__dirname); -const OLDEST_IMAGE = "redis:6.2.24"; +const IMAGES = ["redis:6.2.24", "redis:7.2.16", "redis:7.4.11", "redis:8.2.10", getImage(__dirname)]; const REDISSTACK_IMAGE = getImage(__dirname, 1); -describe.each([IMAGE, OLDEST_IMAGE])("RedisContainer (%s)", { timeout: 240_000 }, (image) => { +describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { it("should connect and execute set-get", async () => { // redisStartContainer { await using container = await new RedisContainer(image).start(); @@ -164,21 +163,6 @@ describe.each([IMAGE, OLDEST_IMAGE])("RedisContainer (%s)", { timeout: 240_000 } }); describe("RedisContainer", { timeout: 240_000 }, () => { - it.each(["redis:7.2.16", "redis:7.4.11", "redis:8.2.10"])( - "should connect and execute set-get with %s", - async (image) => { - await using container = await new RedisContainer(image).start(); - - const client = createClient({ url: container.getConnectionUrl() }); - await client.connect(); - - await client.set("key", "val"); - expect(await client.get("key")).toBe("val"); - - client.destroy(); - } - ); - it("should start with redis-stack-server and json module", async () => { // startWithRedisStack { await using container = await new RedisContainer(REDISSTACK_IMAGE).withPassword("testPassword").start(); diff --git a/packages/modules/valkey/src/valkey-container.test.ts b/packages/modules/valkey/src/valkey-container.test.ts index 9fd58206a..8135479a9 100644 --- a/packages/modules/valkey/src/valkey-container.test.ts +++ b/packages/modules/valkey/src/valkey-container.test.ts @@ -4,10 +4,15 @@ import { createClient } from "redis"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { ValkeyContainer } from "./valkey-container"; -const IMAGE = getImage(__dirname); -const OLDEST_IMAGE = "valkey/valkey:7.2.14"; - -describe.each([IMAGE, OLDEST_IMAGE])("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { +const IMAGES = [ + "valkey/valkey:7.2.14", + "valkey/valkey:8.0.11", + "valkey/valkey:8.1.10", + "valkey/valkey:9.0.6", + getImage(__dirname), +]; + +describe.each(IMAGES)("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { it("should connect and execute set-get", async () => { // valkeyStartContainer { await using container = await new ValkeyContainer(image).start(); @@ -127,20 +132,3 @@ describe.each([IMAGE, OLDEST_IMAGE])("ValkeyContainer (%s)", { timeout: 240_000 // } }); }); - -describe("ValkeyContainer", { timeout: 240_000 }, () => { - it.each(["valkey/valkey:8.0.11", "valkey/valkey:8.1.10", "valkey/valkey:9.0.6"])( - "should connect and execute set-get with %s", - async (image) => { - await using container = await new ValkeyContainer(image).start(); - - const client = createClient({ url: container.getConnectionUrl() }); - await client.connect(); - - await client.set("key", "val"); - expect(await client.get("key")).toBe("val"); - - client.destroy(); - } - ); -}); From 1b3f8c04bbcc61c5be4b4361358c19644fedaf57 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 11:11:51 +0100 Subject: [PATCH 3/5] List one image per supported major version --- .../modules/couchdb/src/couchdb-container.test.ts | 8 ++++---- packages/modules/etcd/src/etcd-container.test.ts | 8 ++++---- packages/modules/nats/src/nats-container.test.ts | 14 +++++++------- packages/modules/redis/src/redis-container.test.ts | 2 +- .../modules/valkey/src/valkey-container.test.ts | 8 +------- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/modules/couchdb/src/couchdb-container.test.ts b/packages/modules/couchdb/src/couchdb-container.test.ts index 8a4ea8a16..2a46976d0 100644 --- a/packages/modules/couchdb/src/couchdb-container.test.ts +++ b/packages/modules/couchdb/src/couchdb-container.test.ts @@ -3,17 +3,17 @@ import { expect } from "vitest"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { CouchDBContainer } from "./couchdb-container"; -const IMAGES = ["couchdb:3.4.3", getImage(__dirname)]; +const IMAGE = getImage(__dirname); type User = { username: string; email: string; }; -describe.each(IMAGES)("CouchDBContainer (%s)", { timeout: 240_000 }, (image) => { +describe("CouchDBContainer", { timeout: 240_000 }, () => { it("should write and read a collection", async () => { // startContainer { - await using container = await new CouchDBContainer(image).start(); + await using container = await new CouchDBContainer(IMAGE).start(); const client = nano({ url: container.getUrl(), @@ -37,7 +37,7 @@ describe.each(IMAGES)("CouchDBContainer (%s)", { timeout: 240_000 }, (image) => it("should use custom credentials", async () => { // customCredentials { - await using container = await new CouchDBContainer(image).withUsername("admin").withPassword("foo").start(); + await using container = await new CouchDBContainer(IMAGE).withUsername("admin").withPassword("foo").start(); const client = nano({ url: container.getUrl(), diff --git a/packages/modules/etcd/src/etcd-container.test.ts b/packages/modules/etcd/src/etcd-container.test.ts index 84644548b..dd28a7ce5 100644 --- a/packages/modules/etcd/src/etcd-container.test.ts +++ b/packages/modules/etcd/src/etcd-container.test.ts @@ -2,12 +2,12 @@ import { Etcd3 } from "etcd3"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { EtcdContainer } from "./etcd-container"; -const IMAGES = ["quay.io/coreos/etcd:v3.5.34", "quay.io/coreos/etcd:v3.6.15", getImage(__dirname)]; +const IMAGE = getImage(__dirname); -describe.each(IMAGES)("EtcdContainer (%s)", { timeout: 180_000 }, (image) => { +describe("EtcdContainer", { timeout: 180_000 }, () => { it("should connect and perform read/write operations", async () => { // readWrite { - await using container = await new EtcdContainer(image).start(); + await using container = await new EtcdContainer(IMAGE).start(); const client = new Etcd3({ hosts: container.getClientEndpoint(), @@ -24,7 +24,7 @@ describe.each(IMAGES)("EtcdContainer (%s)", { timeout: 180_000 }, (image) => { it("should subscribe to key changes", async () => { // etcdSubscribe { - await using container = await new EtcdContainer(image).start(); + await using container = await new EtcdContainer(IMAGE).start(); const client = new Etcd3({ hosts: container.getClientEndpoint(), diff --git a/packages/modules/nats/src/nats-container.test.ts b/packages/modules/nats/src/nats-container.test.ts index db465a759..39800661c 100644 --- a/packages/modules/nats/src/nats-container.test.ts +++ b/packages/modules/nats/src/nats-container.test.ts @@ -3,9 +3,9 @@ import { connect } from "@nats-io/transport-node"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { NatsContainer } from "./nats-container"; -const IMAGES = ["nats:2.14.7-alpine", getImage(__dirname)]; +const IMAGE = getImage(__dirname); -describe.each(IMAGES)("NatsContainer (%s)", { timeout: 180_000 }, (image) => { +describe("NatsContainer", { timeout: 180_000 }, () => { it("should subscribe and receive one published message", async () => { // natsPubsub { const SUBJECT = "HELLO"; @@ -13,7 +13,7 @@ describe.each(IMAGES)("NatsContainer (%s)", { timeout: 180_000 }, (image) => { const TE = new TextEncoder(); const TD = new TextDecoder(); - await using container = await new NatsContainer(image).start(); + await using container = await new NatsContainer(IMAGE).start(); const nc = await connect(container.getConnectionOptions()); const sub = nc.subscribe(SUBJECT); @@ -36,7 +36,7 @@ describe.each(IMAGES)("NatsContainer (%s)", { timeout: 180_000 }, (image) => { it("should start with alternative username and password ", async () => { // natsCredentials { - await using container = await new NatsContainer(image).withUsername("George").withPass("1234").start(); + await using container = await new NatsContainer(IMAGE).withUsername("George").withPass("1234").start(); // } const nc = await connect(container.getConnectionOptions()); @@ -48,7 +48,7 @@ describe.each(IMAGES)("NatsContainer (%s)", { timeout: 180_000 }, (image) => { it("should start with JetStream ", async () => { // natsJetstream { - await using container = await new NatsContainer(image).withJetStream().start(); + await using container = await new NatsContainer(IMAGE).withJetStream().start(); // } const nc = await connect(container.getConnectionOptions()); @@ -60,7 +60,7 @@ describe.each(IMAGES)("NatsContainer (%s)", { timeout: 180_000 }, (image) => { }); it("should fail without JetStream ", async () => { - await using container = await new NatsContainer(image).start(); + await using container = await new NatsContainer(IMAGE).start(); const nc = await connect(container.getConnectionOptions()); @@ -79,7 +79,7 @@ describe.each(IMAGES)("NatsContainer (%s)", { timeout: 180_000 }, (image) => { // for the complete list of available arguments see: // See Command Line Options section inside [NATS docker image documentation](https://hub.docker.com/_/nats) async function outputVersionAndExit() { - await using container = await new NatsContainer(image).withArg("version", "").start(); + await using container = await new NatsContainer(IMAGE).withArg("version", "").start(); await connect(container.getConnectionOptions()); } diff --git a/packages/modules/redis/src/redis-container.test.ts b/packages/modules/redis/src/redis-container.test.ts index 19650494a..83b1b16cf 100644 --- a/packages/modules/redis/src/redis-container.test.ts +++ b/packages/modules/redis/src/redis-container.test.ts @@ -4,7 +4,7 @@ import { createClient } from "redis"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { RedisContainer } from "./redis-container"; -const IMAGES = ["redis:6.2.24", "redis:7.2.16", "redis:7.4.11", "redis:8.2.10", getImage(__dirname)]; +const IMAGES = ["redis:6.2.24", "redis:7.4.11", getImage(__dirname)]; const REDISSTACK_IMAGE = getImage(__dirname, 1); describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { diff --git a/packages/modules/valkey/src/valkey-container.test.ts b/packages/modules/valkey/src/valkey-container.test.ts index 8135479a9..0ddd88a42 100644 --- a/packages/modules/valkey/src/valkey-container.test.ts +++ b/packages/modules/valkey/src/valkey-container.test.ts @@ -4,13 +4,7 @@ import { createClient } from "redis"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { ValkeyContainer } from "./valkey-container"; -const IMAGES = [ - "valkey/valkey:7.2.14", - "valkey/valkey:8.0.11", - "valkey/valkey:8.1.10", - "valkey/valkey:9.0.6", - getImage(__dirname), -]; +const IMAGES = ["valkey/valkey:7.2.14", "valkey/valkey:8.1.10", getImage(__dirname)]; describe.each(IMAGES)("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { it("should connect and execute set-get", async () => { From 03d42c5563c654ea32da1c372eb7a012bfc4aca7 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 12:44:42 +0100 Subject: [PATCH 4/5] Run the images one after another and each image's tests concurrently --- .../modules/redis/src/redis-container.test.ts | 20 +++++++++---------- .../valkey/src/valkey-container.test.ts | 16 +++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/modules/redis/src/redis-container.test.ts b/packages/modules/redis/src/redis-container.test.ts index 83b1b16cf..5bbdd2264 100644 --- a/packages/modules/redis/src/redis-container.test.ts +++ b/packages/modules/redis/src/redis-container.test.ts @@ -7,8 +7,8 @@ import { RedisContainer } from "./redis-container"; const IMAGES = ["redis:6.2.24", "redis:7.4.11", getImage(__dirname)]; const REDISSTACK_IMAGE = getImage(__dirname, 1); -describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { - it("should connect and execute set-get", async () => { +describe.each(IMAGES)("RedisContainer (%s)", { concurrent: false, timeout: 240_000 }, (image) => { + it.concurrent("should connect and execute set-get", async () => { // redisStartContainer { await using container = await new RedisContainer(image).start(); @@ -22,7 +22,7 @@ describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { // } }); - it("should connect with password and execute set-get", async () => { + it.concurrent("should connect with password and execute set-get", async () => { await using container = await new RedisContainer(image).withPassword("test").start(); const client = createClient({ url: container.getConnectionUrl() }); @@ -34,7 +34,7 @@ describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { client.destroy(); }); - it("should reconnect with volume and persistence data", async () => { + it.concurrent("should reconnect with volume and persistence data", async () => { // persistentData { const sourcePath = fs.mkdtempSync("redis-"); @@ -56,7 +56,7 @@ describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { // } }); - it("should load initial data and can read it", async () => { + it.concurrent("should load initial data and can read it", async () => { // withPredefinedData { await using container = await new RedisContainer(image) .withPassword("test") @@ -77,7 +77,7 @@ describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { // } }); - it("should load initial data once", async () => { + it.concurrent("should load initial data once", async () => { await using container = await new RedisContainer(image) .withInitialData(path.join(__dirname, "initCount.redis")) .start(); @@ -90,7 +90,7 @@ describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { client.destroy(); }); - it("should start with credentials and login", async () => { + it.concurrent("should start with credentials and login", async () => { // redisStartWithCredentials { const password = "testPassword"; @@ -108,7 +108,7 @@ describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { client.destroy(); }); - it("should execute container cmd and return the result", async () => { + it.concurrent("should execute container cmd and return the result", async () => { // executeCommand { await using container = await new RedisContainer(image).start(); @@ -118,7 +118,7 @@ describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { // } }); - it("should start redis with custom command", async () => { + it.concurrent("should start redis with custom command", async () => { const container = new RedisContainer(image).withCommand(["redis-server", "--loglevel", "verbose"]); await using startedContainer = await container.start(); @@ -134,7 +134,7 @@ describe.each(IMAGES)("RedisContainer (%s)", { timeout: 240_000 }, (image) => { client.destroy(); }); - it("should start redis with custom command and keeping default args", async () => { + it.concurrent("should start redis with custom command and keeping default args", async () => { const sourcePath = fs.mkdtempSync("redis-"); const container = new RedisContainer(image) diff --git a/packages/modules/valkey/src/valkey-container.test.ts b/packages/modules/valkey/src/valkey-container.test.ts index 0ddd88a42..cb303533b 100644 --- a/packages/modules/valkey/src/valkey-container.test.ts +++ b/packages/modules/valkey/src/valkey-container.test.ts @@ -6,8 +6,8 @@ import { ValkeyContainer } from "./valkey-container"; const IMAGES = ["valkey/valkey:7.2.14", "valkey/valkey:8.1.10", getImage(__dirname)]; -describe.each(IMAGES)("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { - it("should connect and execute set-get", async () => { +describe.each(IMAGES)("ValkeyContainer (%s)", { concurrent: false, timeout: 240_000 }, (image) => { + it.concurrent("should connect and execute set-get", async () => { // valkeyStartContainer { await using container = await new ValkeyContainer(image).start(); @@ -21,7 +21,7 @@ describe.each(IMAGES)("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { // } }); - it("should connect with password and execute set-get", async () => { + it.concurrent("should connect with password and execute set-get", async () => { await using container = await new ValkeyContainer(image).withPassword("test").start(); const client = createClient({ url: container.getConnectionUrl() }); @@ -33,7 +33,7 @@ describe.each(IMAGES)("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { client.destroy(); }); - it("should reconnect with volume and persistence data", async () => { + it.concurrent("should reconnect with volume and persistence data", async () => { // valkeyWithPersistentData { const sourcePath = fs.mkdtempSync("valkey-"); @@ -56,7 +56,7 @@ describe.each(IMAGES)("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { // } }); - it("should load initial data and can read it", async () => { + it.concurrent("should load initial data and can read it", async () => { // valkeyWithPredefinedData { await using container = await new ValkeyContainer(image) .withPassword("test") @@ -77,7 +77,7 @@ describe.each(IMAGES)("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { // } }); - it("should start with username and password", async () => { + it.concurrent("should start with username and password", async () => { // valkeyWithUsernameAndPassword { const username = "testUser"; const password = "testPassword"; @@ -98,7 +98,7 @@ describe.each(IMAGES)("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { client.destroy(); }); - it("should start with password only", async () => { + it.concurrent("should start with password only", async () => { // valkeyWithPassword { const password = "testPassword"; @@ -116,7 +116,7 @@ describe.each(IMAGES)("ValkeyContainer (%s)", { timeout: 240_000 }, (image) => { client.destroy(); }); - it("should execute container cmd and return the result", async () => { + it.concurrent("should execute container cmd and return the result", async () => { // valkeyExecuteCommand { await using container = await new ValkeyContainer(image).start(); From 0dac9c388d6d915545ac665bc431743503c714d2 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 18:27:04 +0100 Subject: [PATCH 5/5] Name the Redis Stack test block after its image --- packages/modules/redis/src/redis-container.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/redis/src/redis-container.test.ts b/packages/modules/redis/src/redis-container.test.ts index 5bbdd2264..7f4dc5908 100644 --- a/packages/modules/redis/src/redis-container.test.ts +++ b/packages/modules/redis/src/redis-container.test.ts @@ -162,7 +162,7 @@ describe.each(IMAGES)("RedisContainer (%s)", { concurrent: false, timeout: 240_0 }); }); -describe("RedisContainer", { timeout: 240_000 }, () => { +describe(`RedisContainer (${REDISSTACK_IMAGE})`, { timeout: 240_000 }, () => { it("should start with redis-stack-server and json module", async () => { // startWithRedisStack { await using container = await new RedisContainer(REDISSTACK_IMAGE).withPassword("testPassword").start();