6.4 KiB
testing-integration-postgres
Purpose
PostgreSQL 16 ServiceFixture for integration tests. Provides env-bridge passthrough (POSTGRES_URL), pg_isready in-container health checking, injectable exec function, and configurable database, user, and password. Enables real PostgreSQL integration tests without mocks. Depends on @suaveplan/testing-integration and testcontainers.
Requirements
Requirement: PostgresFixture implements ServiceFixture
The package SHALL export PostgresFixture implementing ServiceFixture<PostgresConfig> with name === "postgres". PostgresConfig SHALL be { url: string; host: string; port: number; database: string; username: string; password: string }.
Scenario: name property is "postgres"
- WHEN an instance's
.nameis read - THEN the value is the literal
"postgres"
Verified by: src/fixture/postgres-fixture.test.ts::"name property returns postgres"
Requirement: Env-bridge passthrough mode
When POSTGRES_URL is set and non-empty, start() MUST parse it via new URL(...) and return the config without starting a container. The fallback port (when omitted) MUST be POSTGRES_CONTAINER_PORT. In passthrough mode stop() MUST be a no-op.
Scenario: parses all config from POSTGRES_URL
- WHEN
POSTGRES_URL=postgresql://u:p@h:5433/dbis set - THEN
start()returns{ url, host, port: 5433, database: "db", username: "u", password: "p" }
Scenario: stop is a no-op in passthrough mode
- WHEN the fixture started in passthrough mode and
stop()is called - THEN no container is touched
Scenario: uses default port when URL has no port
- WHEN the URL omits the port
- THEN the fixture falls back to
POSTGRES_CONTAINER_PORT
Verified by: src/fixture/postgres-fixture.test.ts::"passthrough mode: parses all config from POSTGRES_URL" AND src/fixture/postgres-fixture.test.ts::"passthrough mode: stop is a no-op" AND src/fixture/postgres-fixture.test.ts::"passthrough mode: uses default port when URL has no port"
Requirement: Container mode
When no POSTGRES_URL is set, start() MUST start a Postgres container via the injected containerFactory (or GenericContainer), set POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB env vars, expose POSTGRES_CONTAINER_PORT, and return a config with a populated url.
Scenario: container mode happy path returns correct URL
- WHEN
start()completes container boot - THEN
result.urlreflects the username, password, mapped port, and database
Scenario: stop calls container.stop()
- WHEN
stop()followsstart()in container mode - THEN the container's
stop()is invoked exactly once
Scenario: custom database/username/password reflected in config and URL
- WHEN the fixture is constructed with overrides
- THEN the URL and config fields use those overrides
Scenario: factory falls back to dynamic GenericContainer import
- WHEN no
containerFactoryis injected - THEN the fixture dynamically imports testcontainers and uses
GenericContainer
Verified by: src/fixture/postgres-fixture.test.ts::"container mode happy path: returns correct URL" AND src/fixture/postgres-fixture.test.ts::"container mode: stop calls container.stop()" AND src/fixture/postgres-fixture.test.ts::"custom database/username/password reflected in config and URL" AND src/fixture/postgres-fixture.test.ts::"resolveContainerFactory: falls back to GenericContainer import"
Requirement: pg_isready in-container health check
In container mode, healthCheck() MUST execute pg_isready -U postgres inside the container and return true only when exitCode === 0. The exec function MUST be injectable as options.execFn. Without injection it MUST default to container.exec(cmd). Without a started container, healthCheck() MUST return false.
Scenario: returns true when execFn exit code 0
- WHEN the injected
execFnresolves to{ exitCode: 0 } - THEN
healthCheck()resolves totrue
Scenario: returns false on non-zero exit code
- WHEN the injected
execFnresolves to{ exitCode: 1 } - THEN
healthCheck()resolves tofalse
Scenario: defaults to container.exec when no execFn injected
- WHEN no
execFnis supplied - THEN
healthCheck()calls intocontainer.exec(["pg_isready", "-U", "postgres"])
Scenario: returns false when no container
- WHEN
healthCheck()is called beforestart() - THEN the result is
false
Verified by: src/fixture/postgres-fixture.test.ts::"healthCheck returns true via injected execFn (exitCode 0)" AND src/fixture/postgres-fixture.test.ts::"healthCheck returns false via injected execFn (exitCode 1)" AND src/fixture/postgres-fixture.test.ts::"healthCheck uses container.exec when no execFn provided" AND src/fixture/postgres-fixture.test.ts::"healthCheck returns false when no container"
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/postgres-fixture.test.ts::"stop when no container is a no-op"
Requirement: passthrough healthCheck semantics — documented
In passthrough mode the fixture has no container; 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).
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
Verified by: src/fixture/postgres-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 either symbol
Verified by: src/fixture/postgres-fixture.test.ts::"name property returns postgres"