Files
2026-05-27 23:47:53 +01:00

4.8 KiB

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

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:

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.