7.2 KiB
testing-integration-redis
Purpose
Redis 7 ServiceFixture for integration tests. Provides env-bridge passthrough (REDIS_URL), TCP PING health checking, injectable TCP-ping function, and URL parsing for passthrough mode. Enables real Redis integration tests without mocks. Depends on @suaveplan/testing-integration and testcontainers.
Requirements
Requirement: RedisFixture implements ServiceFixture
The package SHALL export RedisFixture implementing ServiceFixture<RedisConfig> with name === "redis". RedisConfig SHALL be { url: string; host: string; port: number }. The fixture MUST default to the REDIS_IMAGE constant and expose REDIS_CONTAINER_PORT.
Scenario: name property is "redis"
- WHEN an instance's
.nameis read - THEN the value is the literal
"redis"
Verified by: src/fixture/redis-fixture.test.ts::"name property returns redis"
Requirement: Env-bridge passthrough mode
When REDIS_URL is set and non-empty, start() MUST parse it via new URL(...) and return { url, host, port } directly without starting a container. The fallback port (when the URL omits one) MUST be REDIS_CONTAINER_PORT. In passthrough mode stop() MUST be a no-op.
Scenario: parses config from REDIS_URL
- WHEN
REDIS_URL=redis://h:1234is set - THEN
start()returns{ url, host: "h", port: 1234 }without invoking testcontainers
Scenario: uses default port when URL has no port
- WHEN
REDIS_URL=redis://his set - THEN
start()returnsport: REDIS_CONTAINER_PORT
Scenario: stop is a no-op in passthrough mode
- WHEN
start()ran in passthrough mode andstop()is called - THEN no container is touched
Verified by: src/fixture/redis-fixture.test.ts::"passthrough mode: parses config from REDIS_URL" AND src/fixture/redis-fixture.test.ts::"passthrough mode: uses default port when URL has no port" AND src/fixture/redis-fixture.test.ts::"passthrough mode: stop is a no-op"
Requirement: Container mode
When no REDIS_URL is set, start() MUST start a Redis container via the injected containerFactory (or testcontainers GenericContainer when no factory is injected), expose REDIS_CONTAINER_PORT, and return { url: "redis://localhost:<mappedPort>", host: "localhost", port: <mappedPort> }. stop() MUST call container.stop() exactly once. Container image MUST be overridable via options.image.
Scenario: returns correct config from mapped port
- WHEN
start()boots a container that maps the Redis port - THEN
result.url,result.host, andresult.portreflect the mapped port
Scenario: stop calls container.stop()
- WHEN
stop()is called afterstart()in container mode - THEN the container's
stop()is invoked exactly once
Scenario: image option overrides default
- WHEN
new RedisFixture({ image: "x" })is started - THEN the container factory is invoked with
"x"
Scenario: factory falls back to dynamic GenericContainer import
- WHEN no
containerFactoryis injected - THEN the fixture dynamically imports
testcontainersand usesGenericContainer
Verified by: src/fixture/redis-fixture.test.ts::"container mode happy path: returns correct config" AND src/fixture/redis-fixture.test.ts::"container mode: stop calls container.stop()" AND src/fixture/redis-fixture.test.ts::"image override: factory receives custom image" AND src/fixture/redis-fixture.test.ts::"resolveContainerFactory: falls back to GenericContainer import"
Requirement: TCP-PING health check
In container mode, healthCheck() MUST connect to localhost:<mappedPort> over TCP, send PING\r\n, and return true only when the response begins with +PONG. The TCP function MUST be injectable as options.tcpPingFn. In passthrough mode and when no container has been started, healthCheck() MUST return false.
Scenario: returns true when server replies +PONG
- WHEN the injected ping function resolves to
true - THEN
healthCheck()resolves totrue
Scenario: returns false on non-PONG / no listener
- WHEN the injected ping function resolves to
false - THEN
healthCheck()resolves tofalse
Scenario: returns false when no container
- WHEN
healthCheck()is called beforestart()(or after passthrough mode) - THEN the result is
false
Scenario: defaultTcpPing returns false when nothing listening
- WHEN
defaultTcpPing(<unbound port>, "localhost")is called - THEN it resolves to
false
Scenario: defaultTcpPing returns true on +PONG response
- WHEN the server replies
+PONG\r\n - THEN
defaultTcpPingresolves totrue
Scenario: defaultTcpPing returns false on non-PONG response
- WHEN the server replies anything other than
+PONG - THEN
defaultTcpPingresolves tofalse
Verified by: src/fixture/redis-fixture.test.ts::"healthCheck returns true via injected tcpPingFn" AND src/fixture/redis-fixture.test.ts::"healthCheck returns false via injected tcpPingFn" AND src/fixture/redis-fixture.test.ts::"healthCheck returns false when no container" AND src/fixture/redis-fixture.test.ts::"healthCheck defaultTcpPing: returns false when nothing listening" AND src/fixture/redis-fixture.test.ts::"healthCheck defaultTcpPing: returns true when server replies +PONG" AND src/fixture/redis-fixture.test.ts::"healthCheck defaultTcpPing: returns false when server replies non-PONG"
Requirement: stop() is idempotent
stop() MUST be safe to call when no container has been started.
Scenario: stop without start is a no-op
- WHEN
stop()is called on a freshly-constructed fixture - THEN no error is thrown
Verified by: src/fixture/redis-fixture.test.ts::"stop when no container is a no-op"
Requirement: passthrough healthCheck semantics — documented
In passthrough mode the fixture has no container to probe; healthCheck() returns false. Consumers using passthrough mode MUST NOT poll passthrough fixtures via HealthPoller — readiness is the operator's responsibility (the env-bridge URL is assumed to point at an already-healthy service). The TestEnvironment.setup() flow does not poll fixtures, so passthrough remains compatible with the standard lifecycle.
Scenario: passthrough mode does not call container.stop()
- WHEN the fixture started in passthrough mode and
stop()is called - THEN
container.stop()is never invoked (there is no container)
Verified by: src/fixture/redis-fixture.test.ts::"passthrough mode: stop is a no-op"
Requirement: testing-integration is a peer dependency
The package's package.json MUST list @suaveplan/testing-integration and testcontainers in peerDependencies, not in dependencies. They MUST also appear in devDependencies so the workspace symlink resolves for the package's own tests.
Scenario: variant does not bundle the core or testcontainers
- WHEN the published
package.jsonis inspected - THEN
dependenciesdoes not contain@suaveplan/testing-integrationand does not containtestcontainers
Verified by: src/fixture/redis-fixture.test.ts::"name property returns redis" (proves the variant resolves the peer at test time).