Initial Commit
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# HealthPoller
|
||||
|
||||
## Purpose
|
||||
|
||||
`HealthPoller` repeatedly calls a `ServiceFixture`'s `healthCheck()` method at a configurable interval until the service reports healthy or a timeout elapses. It is used internally by `TestEnvironment` during the setup phase, but can also be used standalone when you need fine-grained control over the polling strategy for a single fixture. The injectable `sleep` function makes it fully deterministic in unit tests — no real timers are required.
|
||||
|
||||
## Features
|
||||
|
||||
- **Configurable timeout** — `timeoutMs` (default 120 000 ms) controls the maximum wait window.
|
||||
- **Configurable interval** — `intervalMs` (default 1 000 ms) controls the delay between successive probes.
|
||||
- **Injectable sleep** — supply a fake `sleep` function in tests to avoid real delays and assert exact call counts.
|
||||
- **Structured error** — throws `TestEnvironmentError` (not a generic `Error`) so callers can discriminate on the error type.
|
||||
- **Zero dependencies** — relies only on `ServiceFixture` from the same package; no external runtime dependencies.
|
||||
- **Protected members** — `timeoutMs`, `intervalMs`, and `sleep` are `protected` so subclasses can override behavior.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { HealthPoller } from "@suaveplan/testing-integration/helpers/health-poller";
|
||||
import type { ServiceFixture } from "@suaveplan/testing-integration/fixture/service-fixture";
|
||||
|
||||
const poller = new HealthPoller({ timeoutMs: 30_000, intervalMs: 500 });
|
||||
|
||||
// myFixture.healthCheck() is called every 500 ms until true or 30 s elapses
|
||||
await poller.poll(myFixture);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Inject a fake sleep function in tests so polling logic can be verified without real delays:
|
||||
|
||||
```ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { HealthPoller, TestEnvironmentError } from "@suaveplan/testing-integration/helpers/health-poller";
|
||||
import type { ServiceFixture } from "@suaveplan/testing-integration/fixture/service-fixture";
|
||||
|
||||
function makeFixture(responses: boolean[]): ServiceFixture<Record<string, unknown>> {
|
||||
let i = 0;
|
||||
return {
|
||||
name: "db",
|
||||
start: async () => ({}),
|
||||
stop: async () => {},
|
||||
healthCheck: async () => responses[i++] ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
it("retries until healthy", async () => {
|
||||
const sleepCalls: number[] = [];
|
||||
const poller = new HealthPoller({
|
||||
timeoutMs: 5_000,
|
||||
intervalMs: 200,
|
||||
sleep: async (ms) => { sleepCalls.push(ms); },
|
||||
});
|
||||
|
||||
await poller.poll(makeFixture([false, false, true]));
|
||||
expect(sleepCalls).toEqual([200, 200]);
|
||||
});
|
||||
|
||||
it("throws TestEnvironmentError on timeout", async () => {
|
||||
const poller = new HealthPoller({
|
||||
timeoutMs: 400,
|
||||
intervalMs: 200,
|
||||
sleep: async () => {},
|
||||
});
|
||||
|
||||
await expect(poller.poll(makeFixture([]))).rejects.toBeInstanceOf(TestEnvironmentError);
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `HealthPoller`
|
||||
|
||||
**Constructor:** `new HealthPoller(options?: HealthPollerOptions)`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `timeoutMs` | `number` | `120_000` | Maximum milliseconds to wait before throwing. |
|
||||
| `intervalMs` | `number` | `1_000` | Milliseconds between `healthCheck()` calls. |
|
||||
| `sleep` | `(ms: number) => Promise<void>` | `setTimeout`-based | Injectable sleep for deterministic testing. |
|
||||
|
||||
**Methods:**
|
||||
|
||||
| Method | Signature | Description |
|
||||
|---|---|---|
|
||||
| `poll` | `(fixture: ServiceFixture<…>) => Promise<void>` | Polls the fixture until healthy or timeout. Throws `TestEnvironmentError` on timeout. |
|
||||
|
||||
### `TestEnvironmentError`
|
||||
|
||||
Extends `Error`. The `name` property is always `"TestEnvironmentError"`. Thrown by `HealthPoller.poll()` when the timeout elapses before the fixture becomes healthy.
|
||||
|
||||
### `HealthPollerOptions`
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `timeoutMs` | `number` | No | Maximum wait time in milliseconds. |
|
||||
| `intervalMs` | `number` | No | Polling interval in milliseconds. |
|
||||
| `sleep` | `(ms: number) => Promise<void>` | No | Sleep implementation (injectable for tests). |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The polling loop tracks `elapsed` time by accumulating `intervalMs` on each iteration rather than reading a real wall clock. This makes the poller's behavior fully deterministic when a fake `sleep` is injected — the timeout fires exactly when `elapsed > timeoutMs`, regardless of real time. In production use (default `setTimeout` sleep), elapsed time approximates real time closely enough for integration test purposes.
|
||||
|
||||
The timeout check is performed *after* sleeping rather than before to avoid a race where the first call returns unhealthy but elapsed immediately hits the timeout. With the check after sleep, there is always at least one full `intervalMs` of wait before a timeout is declared. The strict `>` comparison (rather than `>=`) means a fixture whose response latency equals exactly `timeoutMs` still gets one final health check after its sleep window, ensuring the timeout budget is truly exhausted before giving up.
|
||||
Reference in New Issue
Block a user