Testing
Rivet provides a straightforward testing framework to build reliable and maintainable applications. This guide covers how to write effective tests for your actor-based services.
Setup
To set up testing with Rivet:
# Install Vitest
npm install -D vitest
# Run tests
npm test
Basic Testing Setup
Rivet includes a test helper called setupTest that configures a test environment with in-memory drivers for your actors. This allows for fast, isolated tests without external dependencies.
import { test, expect } from "vitest";
import { setupTest } from "rivetkit/test";
import { actor, setup } from "rivetkit";
// Define the actor
const myActor = actor({
state: { value: "initial" },
actions: {
someAction: (c) => {
c.state.value = "updated";
return c.state.value;
},
getState: (c) => {
return c.state.value;
}
}
});
// Create the registry
const registry = setup({
use: { myActor }
});
// Test the actor
test("my actor test", async (testCtx) => {
const { client } = await setupTest(testCtx, registry);
// Now you can interact with your actor through the client
const myActorHandle = client.myActor.get(["test"]);
// Test your actor's functionality
await myActorHandle.someAction();
// Make assertions
const result = await myActorHandle.getState();
expect(result).toEqual("updated");
});
Testing Actor State
The test framework uses in-memory drivers that persist state within each test, allowing you to verify that your actor correctly maintains state between operations.
import { test, expect } from "vitest";
import { setupTest } from "rivetkit/test";
import { actor, setup } from "rivetkit";
// Define the counter actor
const counter = actor({
state: { count: 0 },
actions: {
increment: (c) => {
c.state.count += 1;
c.broadcast("newCount", c.state.count);
return c.state.count;
},
getCount: (c) => {
return c.state.count;
}
}
});
// Create the registry
const registry = setup({
use: { counter }
});
// Test state persistence
test("actor should persist state", async (testCtx) => {
const { client } = await setupTest(testCtx, registry);
const counterHandle = client.counter.get(["test"]);
// Initial state
expect(await counterHandle.getCount()).toBe(0);
// Modify state
await counterHandle.increment();
// Verify state was updated
expect(await counterHandle.getCount()).toBe(1);
});
Testing Events
For actors that emit events, you can verify events are correctly triggered by subscribing to them:
import { test, expect, vi } from "vitest";
import { setupTest } from "rivetkit/test";
import { actor, setup } from "rivetkit";
interface ChatMessage {
username: string;
message: string;
}
// Define the chat room actor
const chatRoom = actor({
state: {
messages: [] as ChatMessage[]
},
actions: {
sendMessage: (c, username: string, message: string) => {
c.state.messages.push({ username, message });
c.broadcast("newMessage", username, message);
},
getHistory: (c) => {
return c.state.messages;
},
},
});
// Create the registry
const registry = setup({
use: { chatRoom }
});
// Test event emission
test("actor should emit events", async (testCtx) => {
const { client } = await setupTest(testCtx, registry);
const chatRoomHandle = client.chatRoom.get(["test"]);
// Set up event handler with a mock function
const mockHandler = vi.fn();
const conn = chatRoomHandle.connect();
conn.on("newMessage", mockHandler);
// Trigger the event
await conn.sendMessage("testUser", "Hello world");
// Wait for the event to be emitted
await vi.waitFor(() => {
expect(mockHandler).toHaveBeenCalledWith("testUser", "Hello world");
});
});
Testing Schedules
Rivet’s schedule functionality can be tested using Vitest’s time manipulation utilities:
import { test, expect, vi } from "vitest";
import { setupTest } from "rivetkit/test";
import { actor, setup } from "rivetkit";
// Define the scheduler actor
const scheduler = actor({
state: {
tasks: [] as string[],
completedTasks: [] as string[]
},
actions: {
scheduleTask: (c, taskName: string, delayMs: number) => {
c.state.tasks.push(taskName);
// Schedule "completeTask" to run after the specified delay
c.schedule.after(delayMs, "completeTask", taskName);
return { success: true };
},
completeTask: (c, taskName: string) => {
// This action will be called by the scheduler when the time comes
c.state.completedTasks.push(taskName);
return { completed: taskName };
},
getCompletedTasks: (c) => {
return c.state.completedTasks;
}
}
});
// Create the registry
const registry = setup({
use: { scheduler }
});
// Test scheduled tasks
test("scheduled tasks should execute", async (testCtx) => {
// setupTest automatically configures vi.useFakeTimers()
const { client } = await setupTest(testCtx, registry);
const schedulerHandle = client.scheduler.get(["test"]);
// Set up a scheduled task
await schedulerHandle.scheduleTask("reminder", 60000); // 1 minute in the future
// Fast-forward time by 1 minute
await vi.advanceTimersByTimeAsync(60000);
// Verify the scheduled task executed
expect(await schedulerHandle.getCompletedTasks()).toContain("reminder");
});
The setupTest function automatically calls vi.useFakeTimers(), allowing you to control time in your tests with functions like vi.advanceTimersByTimeAsync(). This makes it possible to test scheduled operations without waiting for real time to pass.
Best Practices
- Isolate tests: Each test should run independently, avoiding shared state.
- Test edge cases: Verify how your actor handles invalid inputs, concurrent operations, and error conditions.
- Mock time: Use Vitest’s timer mocks for testing scheduled operations.
- Use realistic data: Test with data that resembles production scenarios.
Rivet’s testing framework automatically handles server setup and teardown, so you can focus on writing effective tests for your business logic.
API Reference
test- Test helper functioncreateMemoryDriver- In-memory driver for testscreateFileSystemDriver- Filesystem driver for tests