Testing — Überblick
Actor-Systeme sind naiv schwer zu testen, weil zwei Dinge von außen schwer sind:
- Asynchronität — jedes
tellpassiert später; Assertions müssen warten. - Zeit — Receive-Timeouts, geplante Retries, Gossip-Runden hängen alle von der Uhr ab.
Das TestKit des Frameworks gibt dir Werkzeuge, um beides zu kontrollieren:
| Werkzeug | Löst |
|---|---|
TestKit | Ein einsatzbereites ActorSystem mit leisem Logging + Helper-Methoden. |
TestProbe | Ein Fake-Actor, der Nachrichten für Assertions erfasst. |
ManualScheduler | Ein Virtual-Clock-Scheduler — Zeit schreitet nur fort, wenn du es sagst. |
MultiNodeSpec | Mehrere Cluster-Nodes in einem Prozess für Cluster-Szenario-Tests hochfahren. |
ParallelMultiNodeSpec | Nodes in separaten Prozessen laufen lassen, wenn Isolation zählt. |
Die ersten drei sind für Unit- + Integrationstests eines einzelnen Actor-Systems; die letzten zwei für verteilte Szenarien.
Ein minimaler Test
Abschnitt betitelt „Ein minimaler Test“import { describe, it, expect } from 'bun:test';import { Actor, Props, TestKit } from 'actor-ts/testkit';
type Cmd = { kind: 'inc' } | { kind: 'get'; replyTo: ActorRef<number> };
class Counter extends Actor<Cmd> { private count = 0; override onReceive(cmd: Cmd): void { if (cmd.kind === 'inc') this.count++; else cmd.replyTo.tell(this.count); }}
describe('Counter', () => { it('inkrementiert und antwortet', async () => { const tk = TestKit.create('counter-spec'); const probe = tk.createTestProbe();
const counter = tk.system.spawnAnonymous(Props.create(() => new Counter())); counter.tell({ kind: 'inc' }); counter.tell({ kind: 'inc' }); counter.tell({ kind: 'get', replyTo: probe as ActorRef<number> });
await probe.expectMsg(2); await tk.shutdown(); });});Zwei Dinge, die das TestKit hier erledigt:
TestKit.create(name)baut ein frisches ActorSystem mit leisem Logging — kein Konsolen-Spam während Test-Läufen.probe.expectMsg(2)assertet, dass die nächste Nachricht, die die Probe empfängt, exakt2ist, mit einem Default-Timeout von 3 Sekunden.
tk.shutdown() am Ende ist kritisch — ohne ihn hinterlässt der
Test ein lebendes System + laufenden Dispatcher, und der
Test-Prozess kann hängen.
Die drei Schichten
Abschnitt betitelt „Die drei Schichten“Unit — Actor isoliert
Abschnitt betitelt „Unit — Actor isoliert“const tk = TestKit.create();const probe = tk.createTestProbe();const ref = tk.system.spawnAnonymous(Props.create(() => new MyActor(probe)));ref.tell({ kind: 'do' });await probe.expectMsg('done');await tk.shutdown();MyActor ist mit probe als Callback-Ziel konstruiert — er
tellt die Probe anstelle dessen, was er in Produktion tellen
würde. Tests asserten auf das, was die Probe sieht.
Das ist die kanonische Form — siehe TestKit und TestProbe.
Zeit-deterministisch — ManualScheduler
Abschnitt betitelt „Zeit-deterministisch — ManualScheduler“const { kit, scheduler } = TestKit.withManualScheduler();const probe = kit.createTestProbe();
const ref = kit.system.spawnAnonymous(Props.create(() => new ScheduledThing(probe)));ref.tell({ kind: 'start' });
scheduler.advance(5_000); // virtuelle Zeit springt 5 Sekundenawait probe.expectMsg({ kind: 'fired' });
await kit.shutdown();Actor, die context.timers.startSingleTimer oder
system.scheduler.scheduleOnce nutzen, bekommen ihr Feuern vom
Manual-Scheduler getrieben — kein echtes setTimeout, keine
Flakiness.
Siehe ManualScheduler.
Verteilt — MultiNodeSpec
Abschnitt betitelt „Verteilt — MultiNodeSpec“import { MultiNodeSpec } from 'actor-ts/testkit';
const spec = await MultiNodeSpec.create({ systemName: 'cluster-spec', nodes: 3,});
// `spec.nodes` ist ein Array von ActorSystems, alle in einen Cluster gejoint.const node1Probe = spec.createTestProbe(0);const remote = spec.nodes[1].spawnAnonymous(Props.create(() => new Worker()));remote.tell({ kind: 'do', replyTo: node1Probe });await node1Probe.expectMsg({ kind: 'done' });
await spec.shutdown();Drei Actor-Systeme joinen einen Cluster innerhalb eines Test- Prozesses, mit dem In-Memory-Transport. Nützlich zum Verifizieren von Sharding-Verhalten, Cluster-Singleton- Failover, Distributed-Data-Merge-Semantik — alles ohne ein Docker-Compose-Setup.
Siehe MultiNodeSpec.
Was testen
Abschnitt betitelt „Was testen“Drei Kategorien:
- Reines Verhalten — gib dem Actor eine Sequenz von Nachrichten, assertete auf die Probe. Die meisten Actor-Unit-Tests sehen so aus.
- State + Persistenz — für PersistentActor teste den Recovery-Pfad explizit, indem du den Actor stoppst und neu spawnst. Assertete, dass der wiederhergestellte State mit dem übereinstimmt, was du aus der Event-Sequenz erwarten würdest.
- Verteiltes Verhalten — für Cluster-Features gibt dir MultiNodeSpec einen „real genug”-Cluster, um Sharding- Placement, Singleton-Failover usw. zu testen.
Was nicht testen
Abschnitt betitelt „Was nicht testen“TestKit vs rohes ActorSystem in Tests
Abschnitt betitelt „TestKit vs rohes ActorSystem in Tests“Beides funktioniert. TestKit ist Convenience:
- Default ist
NoopLogger— kein Konsolen-Spam. - Bietet
createTestProbe(),within(ms, fn),shutdown(). - One-Liner-Setup via
TestKit.create().
Für komplexe Test-Setups (eigener Logger, mehrere Systeme,
spezifische Extensions) liest sich rohes ActorSystem.create(...)
manchmal klarer. Beides produziert dasselbe Actor-Verhalten.
Wo es weitergeht
Abschnitt betitelt „Wo es weitergeht“- TestKit — die Convenience- Fassade um ActorSystem für Tests.
- TestProbe — der Fake-Actor für Assertions.
- ManualScheduler — Virtual-Time-Scheduler für deterministische Timer-Tests.
- MultiNodeSpec — Multi-Node-Cluster-Tests in einem Prozess.
- ParallelMultiNodeSpec — Multi-Node-Cluster-Tests in separaten Prozessen.