TestKit
TestKit is a thin convenience wrapper around ActorSystem for
tests. It gives you a ready-to-use system with sensible test
defaults — quiet logger, easy probe creation, deterministic
shutdown.
import { TestKit } from 'actor-ts/testkit';
const tk = TestKit.create('my-spec');const probe = tk.createTestProbe();const ref = tk.system.actorOf(Props.create(() => new Worker(probe)));
ref.tell({ kind: 'do' });await probe.expectMsg('done');
await tk.shutdown();Three things the kit does for you:
- Builds the
ActorSystemwith aNoopLoggerby default — no console spam during test runs. - Exposes
createTestProbe()so you don’t have to thread the system into every probe constructor. - Owns
shutdown()— call once at test end; safely tears down the system, scheduler, and dispatcher.
The API
Section titled “The API”class TestKit { static create(name?: string, opts?: TestKitOptions): TestKit; static withManualScheduler(name?, opts?): { kit: TestKit; scheduler: ManualScheduler };
readonly system: ActorSystem;
createTestProbe(opts?: TestProbeOptions): TestProbe; within<T>(durationMs: number, fn: () => Promise<T>): Promise<T>; shutdown(): Promise<void>;}
interface TestKitOptions extends ActorSystemSettings { quiet?: boolean; // default true — use NoopLogger}TestKit.create(name?, options?)
Section titled “TestKit.create(name?, options?)”Builds a kit with a fresh system. The name is the system name —
useful when running parallel test suites that mustn’t share state.
Defaults to 'test-kit'.
const tk = TestKit.create('my-spec', { quiet: false, // turn logging on logLevel: LogLevel.Debug, config: { /* HOCON overrides */ },});With quiet: false you get console logs — useful when debugging a
failing test. Otherwise, log calls are no-ops, which keeps your
test output clean.
tk.createTestProbe(options?)
Section titled “tk.createTestProbe(options?)”const probe = tk.createTestProbe({ defaultTimeoutMs: 5_000, // default wait for expect/receive name: 'order-probe', // visible in logs});
ref.tell({ kind: 'do', replyTo: probe });await probe.expectMsg({ kind: 'done' });Returns a TestProbe scoped to this kit’s system. See
TestProbe for the full
expectations API.
tk.within(durationMs, fn)
Section titled “tk.within(durationMs, fn)”await tk.within(1_000, async () => { ref.tell({ kind: 'do' }); await probe.expectMsg('done');});Run fn and assert it completed in under durationMs.
Throws if it took longer. Useful for “this should be fast” SLAs
inside tests, without depending on the test runner’s per-test
timeout.
Note: this is a soft deadline — fn still runs to completion
even if it overshoots. It just throws after the fact.
tk.shutdown()
Section titled “tk.shutdown()”afterEach(async () => { await tk.shutdown();});Terminates the underlying system. Always call this in a fixture teardown — without it, the test process leaks a live system that keeps the event loop alive. Some test runners (Bun, Vitest) detect this and fail the suite; others hang.
TestKit.withManualScheduler()
Section titled “TestKit.withManualScheduler()”const { kit, scheduler } = TestKit.withManualScheduler('my-spec');
const ref = kit.system.actorOf(Props.create(() => new Heartbeat(probe)));ref.tell({ kind: 'start' });
scheduler.advance(5_000); // jump 5 virtual secondsawait probe.expectMsg('tick');Returns both the kit and its ManualScheduler so the test can
advance virtual time. See
ManualScheduler for the
full virtual-clock semantics.
Fixture patterns
Section titled “Fixture patterns”Per-test fixture (Bun, Vitest, Jest)
Section titled “Per-test fixture (Bun, Vitest, Jest)”import { describe, it, beforeEach, afterEach } from 'bun:test';
describe('Counter', () => { let tk: TestKit; let probe: TestProbe;
beforeEach(() => { tk = TestKit.create(); probe = tk.createTestProbe(); });
afterEach(async () => { await tk.shutdown(); });
it('increments', async () => { const ref = tk.system.actorOf(Props.create(() => new Counter())); ref.tell({ kind: 'inc' }); ref.tell({ kind: 'get', replyTo: probe as ActorRef<number> }); await probe.expectMsg(1); });});A fresh kit per test — no state leakage between tests, slightly slower than sharing one kit but worth it for isolation.
Shared fixture for read-only tests
Section titled “Shared fixture for read-only tests”describe('PureBehaviorTests', () => { let tk: TestKit;
beforeAll(() => { tk = TestKit.create(); }); afterAll(async () => { await tk.shutdown(); });
// each `it` uses tk.createTestProbe() for its own probe, but the // underlying ActorSystem is shared.});Faster than per-test setup; only safe when tests don’t mutate shared actors.
What TestKit doesn’t do
Section titled “What TestKit doesn’t do”Where to next
Section titled “Where to next”- Testing overview — the big picture: unit, time-deterministic, multi-node.
- TestProbe — the fake-actor expectations API.
- ManualScheduler — virtual time.
- MultiNodeSpec — for cluster scenarios.
The TestKit API reference
covers the full surface.