Zum Inhalt springen
Deutsch

Testing — Überblick

Actor-Systeme sind naiv schwer zu testen, weil zwei Dinge von außen schwer sind:

  • Asynchronität — jedes tell passiert 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:

WerkzeugLöst
TestKitEin einsatzbereites ActorSystem mit leisem Logging + Helper-Methoden.
TestProbeEin Fake-Actor, der Nachrichten für Assertions erfasst.
ManualSchedulerEin Virtual-Clock-Scheduler — Zeit schreitet nur fort, wenn du es sagst.
MultiNodeSpecMehrere Cluster-Nodes in einem Prozess für Cluster-Szenario-Tests hochfahren.
ParallelMultiNodeSpecNodes 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.

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, exakt 2 ist, 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.

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.

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 Sekunden
await 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.

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.

Drei Kategorien:

  1. Reines Verhalten — gib dem Actor eine Sequenz von Nachrichten, assertete auf die Probe. Die meisten Actor-Unit-Tests sehen so aus.
  2. 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.
  3. Verteiltes Verhalten — für Cluster-Features gibt dir MultiNodeSpec einen „real genug”-Cluster, um Sharding- Placement, Singleton-Failover usw. zu testen.

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.

  • 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.