Zum Inhalt springen
Deutsch

TestProbe

Ein TestProbe ist eine ActorRef, die keinen User-Code ausführt. Stattdessen landet jedes tell in einem Buffer, auf den der Test asserten kann. Übergib eine Probe überall, wo du einen echten Actor übergeben würdest; der zu testende Actor sieht eine normale Ref und tellt sie wie üblich.

const tk = TestKit.create();
const probe = tk.createTestProbe();
const ref = tk.system.spawnAnonymous(Props.create(() => new Worker(probe)));
// ^-- Probe anstelle des echten Downstreams
ref.tell({ kind: 'work' });
await probe.expectMsg({ kind: 'work-done' });
await tk.shutdown();

Worker tellt den „Downstream” — der tatsächlich die Probe ist. Die Probe erfasst die Nachricht; expectMsg konsumiert sie und assertet die Form.

const probe = tk.createTestProbe({
defaultTimeoutMs: 5_000, // Default-Wartezeit für expect/receive — Default 3s
name: 'order-probe',
});

Beide Optionen sind optional. Ohne defaultTimeoutMs wartet jeder expect*-Aufruf bis zu 3 Sekunden. Ohne name bekommt die Probe einen auto-generierten Pfad.

Die Probe erbt von ActorRef, sodass du sie überall übergeben kannst, wo eine ActorRef<T> erwartet wird — obwohl sie als unknown typisiert ist (sie akzeptiert jede Nachrichten-Art). Caste, falls nötig:

ref.tell({ kind: 'get', replyTo: probe as ActorRef<number> });
await probe.expectMsg('done');
await probe.expectMsg({ kind: 'reply', value: 42 });

Wartet auf die nächste Nachricht, vergleicht sie deep-equal mit expected. Wirft, wenn die Form nicht passt oder das Timeout abläuft. Gibt die Nachricht zurück, sodass du Assertions ketten kannst.

import { Success } from 'actor-ts';
const reply = await probe.expectMsgType(Success);
expect(reply.value).toBe(42);

Wartet auf die nächste Nachricht; assertet, dass sie eine Instanz von Class ist. Nützlich für Success- / Failure- Envelopes aus pipeTo oder beliebige klassenbasierte Message-Typen.

ref.tell({ kind: 'do-nothing' });
await probe.expectNoMessage(200);
// → assertet, dass in 200ms keine Nachricht kam

Das Gegenteil von expectMsg — assertet, dass im gegebenen Fenster nichts ankommt. Nützlich für „dieses Kommando sollte still ignoriert werden” oder „der Actor sollte darauf nicht antworten.”

Das Default-Fenster sind 300 ms — kurz genug, um Tests schnell zu halten, lang genug, um die meisten unerwünschten Sends zu fangen.

const msg = await probe.receiveOne();
const msgs = await probe.receiveN(3);

Hole die nächste(n) Nachricht(en), ohne zu asserten. Nutze es, wenn die Form variiert und du sie mit eigener Logik inspizieren willst:

const msg = await probe.receiveOne();
if (msg.kind === 'success') expect(msg.value).toBeGreaterThan(0);
else expect(msg.error).toBeDefined();
const ready = await probe.fishForMessage(
(m) => typeof m === 'object' && m.kind === 'ready',
5_000,
);

Verwirf Nachrichten, bis eine das Prädikat erfüllt. Nützlich, wenn der zu testende Actor einen Strom von Nachrichten emittiert und dich nur eine bestimmte Art interessiert. Verworfene Nachrichten sind weg — sie können später nicht mehr assertet werden.

expect(probe.messageCount).toBe(0);
ref.tell({ kind: 'do' });
await new Promise(r => setImmediate(r)); // einmal yielden, damit der tell ankommt
expect(probe.hasMessage()).toBe(true);

Synchrone Inspektion — kein Warten. Nutze, um zu prüfen „gibt es gequeuete Nachrichten, die ich noch nicht konsumiert habe?”

ref.tell({ kind: 'get', replyTo: probe });
await probe.expectMsg({ kind: 'reply', value: 42 });
expect(probe.sender).toBe(ref); // wer die zuletzt empfangene Nachricht gesendet hat

Nach expectMsg / receiveOne / etc. hält probe.sender den Sender der eben konsumierten Nachricht. Nützlich, um zu asserten, dass „diese Antwort vom richtigen Actor kam.”

probe.reply(msg) ist eine Abkürzung, die dem letzten Sender tellt:

ref.tell({ kind: 'request' }, probe);
await probe.expectMsg({ kind: 'request' });
probe.reply({ kind: 'response' });
// ↑ dasselbe wie: probe.sender.tell({ kind: 'response' }, probe);

Gängig in Tests, die Zwei-Wege-Konversationen simulieren — lass die Probe einen Request empfangen, dann antworten lassen.

ref.tell({ kind: 'one' });
ref.tell({ kind: 'two' });
probe.clearInbox(); // verwirft beide, Queue ist wieder leer
ref.tell({ kind: 'three' });
await probe.expectMsg({ kind: 'three' });

Nützlich zwischen Testphasen, wenn du Setup-Phasen-Nachrichten ignorieren willst und nur auf das asserten möchtest, was als Nächstes kommt.

Probe als Stellvertreter für einen Downstream-Actor

Abschnitt betitelt „Probe als Stellvertreter für einen Downstream-Actor“
class OrderHandler extends Actor<OrderCmd> {
constructor(private readonly db: ActorRef<DbCmd>) { super(); }
override onReceive(cmd: OrderCmd): void {
this.db.tell({ kind: 'insert', orderId: cmd.id });
}
}
it('leitet Orders an db weiter', async () => {
const dbProbe = tk.createTestProbe();
const handler = tk.system.spawnAnonymous(Props.create(() =>
new OrderHandler(dbProbe as ActorRef<DbCmd>)));
handler.tell({ kind: 'place-order', id: 'o-1' });
await dbProbe.expectMsg({ kind: 'insert', orderId: 'o-1' });
});

Die Probe ersetzt einen echten DB-Actor; der Test sieht den Call, ohne dass die DB existieren muss.

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);

Für Ask-artige Nachrichten — die Probe steht für den Fragenden.

const a = tk.createTestProbe({ name: 'a-probe' });
const b = tk.createTestProbe({ name: 'b-probe' });
const router = tk.system.spawnAnonymous(...);
router.tell({ kind: 'send', target: 'a', payload: 'hello' });
await a.expectMsg('hello');
await b.expectNoMessage(100);

Routing-Logik verifizieren, indem du den Inbox jeder Probe prüfst.

Die TestProbe-API-Referenz deckt die volle Oberfläche ab.