ManualScheduler
ManualScheduler ist eine Scheduler-Implementierung, bei der
Zeit nicht von selbst voranschreitet. Plane einen
5-Sekunden-Timer; nichts feuert, bis du explizit
scheduler.advance(5_000) aufrufst. Der Test läuft in
deterministischer virtueller Zeit, ohne Echt-Uhr-
Abhängigkeiten — keine flakigen „schlafe lang genug”-Muster.
import { TestKit } from 'actor-ts/testkit';
const { kit, scheduler } = TestKit.withManualScheduler();const probe = kit.createTestProbe();
const ref = kit.system.spawnAnonymous(Props.create(() => new Heartbeat(probe)));ref.tell({ kind: 'start' });
scheduler.advance(5_000); // virtuelle Zeit springt 5 Sekundenawait probe.expectMsg('tick');
await kit.shutdown();Heartbeat plant einen Tick via
context.timers.startSingleTimer('tick', tickMsg, 5_000). Echte
Zeit verstreicht nie; der advance(5_000) feuert den Timer.
Warum das wichtig ist
Abschnitt betitelt „Warum das wichtig ist“// Ohne ManualScheduler:ref.tell({ kind: 'start' });await new Promise(r => setTimeout(r, 5_100)); // hoffe, echte Zeit verstrichawait probe.expectMsg('tick');Das funktioniert meistens. Dann läuft CI langsamer; der Test
schläft 5,1 s, aber der Scheduler feuert bei 5,0 s, während
expectMsg bei 5,1 s prüft — flakey. Oder deine lokale Maschine
ist schnell; der Test wartet jedes Mal 5,1 s, obwohl der Timer
bei 5,0 s feuert — langsam.
ManualScheduler entfernt die Abhängigkeit von echter Zeit.
Tests sind schnell (kein Warten) und deterministisch (advance
feuert Timer in Reihenfolge).
Die API
Abschnitt betitelt „Die API“class ManualScheduler extends Scheduler { now(): number; // aktuelle virtuelle Zeit (ms) advance(durationMs: number): void; // vorwärts springen, gereifte Timer feuern advanceTo(absoluteMs: number): void; // zu einer spezifischen virtuellen Zeit springen pendingCount: number; // nicht abgebrochene geplante Aufgaben}Vom Scheduler geerbt:
scheduleOnce(delayMs, target, message, sender?): CancellablescheduleOnceFn(delayMs, fn): CancellablescheduleAtFixedRate(initialDelay, interval, target, message, sender?): CancellablescheduleAtFixedRateFn(initialDelay, interval, fn): CancellableEchtes setTimeout wird nicht genutzt. Jeder Schedule ist
eine Virtual-Time-Aufgabe, die nur via advance feuert.
Über TestKit nutzen
Abschnitt betitelt „Über TestKit nutzen“const { kit, scheduler } = TestKit.withManualScheduler('my-spec');Zwei Werte:
kit— ein normalesTestKit, dessensystemden Manual-Scheduler verdrahtet hat.scheduler— der Manual-Scheduler. Rufeadvancedarauf, um Timer zu feuern.
Wenn der zu testende Actor context.timers.startSingleTimer
oder system.scheduler.scheduleOnce nutzt, routet das Framework
durch den Scheduler des Systems — der hier der manuelle ist.
Virtuelle Zeit kontrolliert alle Actor-Timer.
Ohne TestKit nutzen
Abschnitt betitelt „Ohne TestKit nutzen“import { ActorSystem, ManualScheduler } from 'actor-ts';
const scheduler = new ManualScheduler();const system = ActorSystem.create('my-spec', { scheduler });
// ... Test-Code ...
await system.terminate();Gleiche Idee — übergib den Manual-Scheduler an
ActorSystem.create via die Settings.
Zeit voranbringen
Abschnitt betitelt „Zeit voranbringen“scheduler.advance(1_000); // 1 Sekunde springen, Timer in dem Fenster feuernscheduler.advanceTo(5_000); // zu virtueller Zeit = 5000 ms springenscheduler.now(); // → 5000advance(ms) ist der gängige Fall. Aufgaben, die im
voraneilenden Fenster feuern sollen, laufen in Reihenfolge,
sequenziell, bevor advance zurückkehrt.
advanceTo(absoluteMs) ist für den Fall, dass du eine Zielzeit
aufgenommen hast und dorthin vorspringen willst (nützlich für
„nach-N-von-etwas”-Muster zu testen).
Wiederholende Timer
Abschnitt betitelt „Wiederholende Timer“ref.tell({ kind: 'start-heartbeat' }); // plant alle 1sscheduler.advance(3_500);// → feuert bei virtuell 1000, 2000, 3000 (dreimal)Eine scheduleAtFixedRate-Aufgabe feuert in jedem Intervall,
während virtuelle Zeit jedes Vielfache überquert. advance(3500)
ab t=0 feuert die Aufgabe dreimal.
Reihenfolge zählt
Abschnitt betitelt „Reihenfolge zählt“Innerhalb eines advance feuern Aufgaben in ihrer geplanten
Reihenfolge (früheste zuerst; Gleichstände werden durch
Einfüge-Reihenfolge gebrochen). Wenn eine Aufgabe nach dem
Feuern weitere Aufgaben plant (z. B. ein Heartbeat re-armed),
werden diese der Queue hinzugefügt und feuern, falls ihre
Planungszeit ins selbe advance-Fenster fällt.
Ein praktisches Test-Muster
Abschnitt betitelt „Ein praktisches Test-Muster“import { describe, it, beforeEach, afterEach } from 'bun:test';import { TestKit, type ManualScheduler } from 'actor-ts/testkit';
describe('Session-Expiry', () => { let kit: TestKit; let scheduler: ManualScheduler; let probe: TestProbe;
beforeEach(() => { const setup = TestKit.withManualScheduler(); kit = setup.kit; scheduler = setup.scheduler; probe = kit.createTestProbe(); });
afterEach(async () => { await kit.shutdown(); });
it('läuft nach 15 Minuten Inaktivität ab', async () => { const session = kit.system.spawnAnonymous(Props.create(() => new Session(probe, 'user-42')));
session.tell({ kind: 'activity' }); // setzt den Timer zurück scheduler.advance(14 * 60_000); await probe.expectNoMessage(0); // noch nicht abgelaufen
scheduler.advance(60_000 + 1); // überquert 15 Min await probe.expectMsg({ kind: 'session-expired', userId: 'user-42' }); });});Der Test läuft in Mikrosekunden Wall-Clock-Zeit, übt aber ein 15-Minuten-Idle-Timeout exakt aus.
Interaktion mit await und Microtasks
Abschnitt betitelt „Interaktion mit await und Microtasks“ref.tell({ kind: 'start' });scheduler.advance(5_000);await probe.expectMsg('tick');Zweistufige Verarbeitung:
advancefeuert den Timer synchron — ertellt den Actor.- Der
tellwird vom Dispatcher in der nächsten Microtask- Runde verarbeitet, die läuft, wenn du etwasawaitest.
expectMsg gibt eine Promise zurück, die intern awaitet —
also wurde der Tick bis zur Auflösung verarbeitet und die
Nachricht ist im Buffer der Probe.
Du musst meistens nicht darüber nachdenken. Aber wenn du
synchron nach advance zu probe.messageCount greifst und 0
siehst, wurde der Actor noch nicht dispatched — await etwas,
um zu yielden.
Stolperfallen
Abschnitt betitelt „Stolperfallen“Wo es weitergeht
Abschnitt betitelt „Wo es weitergeht“- TestKit — die Fassade mit
withManualScheduler(). - Timer und Scheduling — die Timer-API, die der Scheduler antreibt.
- Receive-Timeout — ein weiterer zeitgetriebener Mechanismus, der von virtueller Zeit profitiert.
- Testing — Überblick — das größere Bild.
Die ManualScheduler-API-
Referenz deckt die volle Oberfläche ab.