# TestEnvironment _(no submodules)_ ## Purpose `TestEnvironment` is the orchestrator for integration test suites that need several external services running concurrently. It accepts a named map of `ServiceFixture` instances, starts them all in parallel during `setup()`, records which ones failed to start, and stops the successful ones in reverse order during `teardown()`. This reverse-teardown order prevents cascading failures in services that depend on each other (e.g. stopping a connection pool before closing its database). ## Features - **Parallel startup** — all fixtures are started with `Promise.allSettled`, so a slow service does not block faster ones. - **Graceful failure handling** — if a fixture's `start()` throws, its name is added to the `failed` list rather than propagating the error; tests can then use `skipIfUnavailable` to opt out. - **Reverse-order teardown** — fixtures are stopped in the reverse of their start-success order, respecting implicit dependency chains. - **Swallowed stop errors** — `stop()` errors are caught and discarded so a single broken fixture does not prevent the rest from cleaning up. - **Safe empty teardown** — calling `teardown()` before `setup()` (or when all fixtures failed) is a no-op. - **Protected internals** — `fixtures`, `options`, and `started` are `protected` to allow subclassing. ## Usage: Basic Example ```ts import { TestEnvironment } from "@suaveplan/testing-integration/environment/test-environment"; const env = new TestEnvironment({ redis: redisFixture, postgres: postgresFixture, }); const { configs, failed } = await env.setup(); if (failed.length > 0) { console.warn("Some fixtures failed to start:", failed); } console.log(configs.redis); // { url: "redis://localhost:6380" } console.log(configs.postgres); // { host: "localhost", port: 5433 } await env.teardown(); ``` ## Usage: Advanced Example Using `TestEnvironment` with `skipIfUnavailable` via the global-setup bridge: ```ts // vitest.config.ts import { createIntegrationSetup } from "@suaveplan/testing-integration/setup/create-integration-setup"; import { redisFixture } from "./fixtures/redis.js"; import { postgresFixture } from "./fixtures/postgres.js"; const { globalSetup, inject } = createIntegrationSetup({ redis: redisFixture, postgres: postgresFixture, }); export default defineConfig({ test: { globalSetup: [globalSetup], }, }); // my.test.ts import { beforeEach } from "vitest"; import { skipIfUnavailable } from "@suaveplan/testing-integration/helpers/skip-if-unavailable"; beforeEach(skipIfUnavailable("redis")); it("stores a value in redis", async () => { // guaranteed: redis was available when setup ran }); ``` ## API Reference ### `TestEnvironment` **Constructor:** `new TestEnvironment(fixtures, options?)` | Parameter | Type | Description | |---|---|---| | `fixtures` | `Record>` | Named map of fixtures to manage. | | `options` | `TestEnvironmentOptions` | Optional configuration (currently `timeoutMs` reserved). | **Methods:** | Method | Signature | Description | |---|---|---| | `setup` | `() => Promise` | Starts all fixtures in parallel. Returns configs and failed list. | | `teardown` | `() => Promise` | Stops all started fixtures in reverse order. | ### `TestEnvironmentSetupResult` | Property | Type | Description | |---|---|---| | `configs` | `Record>` | Config objects keyed by fixture key for successful fixtures. | | `failed` | `string[]` | Names of fixtures whose `start()` threw. | ### `TestEnvironmentOptions` | Property | Type | Description | |---|---|---| | `timeoutMs` | `number?` | Reserved for future per-fixture start timeout. | ### `TestEnvironmentError` Re-exported from `health-poller`. Use `instanceof TestEnvironmentError` to discriminate environment errors. ## Implementation Notes `Promise.allSettled` is used instead of `Promise.all` so that one failing fixture does not prevent others from starting. The index of each settled result maps 1:1 with the `entries` array, so the correct fixture name can be attributed to each failure. The `started` array tracks fixture **names** (not keys) in insertion order. During teardown, names are reversed and matched against `Object.values(this.fixtures)` by name. This means duplicate names across different keys would cause a teardown ambiguity — callers should ensure fixture names are unique within a `TestEnvironment` instance.