Skip to main content
More

Testing

Test Rivet Actors with the built-in framework, create isolated actor instances, invoke actions, and verify state, events, and lifecycle behavior.

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 starts your registry in test mode and returns a client connected to it. 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({
  vars: { value: "initial" },
  actions: {
    someAction: (c) => {
      c.vars.value = "updated";
      return c.vars.value;
    },
    getValue: (c) => {
      return c.vars.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.getOrCreate(["test"]);

  // Test your actor's functionality
  await myActorHandle.someAction();

  // Make assertions
  const result = await myActorHandle.getValue();
  expect(result).toEqual("updated");
});

Testing Persisted Data

Data an actor writes to SQLite persists within each test, allowing you to verify that your actor correctly maintains data between operations.

import { test, expect } from "vitest";
import { setupTest } from "rivetkit/test";
import { actor, setup } from "rivetkit";
import { db } from "rivetkit/db";

// Define the counter actor
const counter = actor({
  db: db({
    onMigrate: async (db) => {
      await db.execute(
        "CREATE TABLE IF NOT EXISTS counter (id INTEGER PRIMARY KEY CHECK (id = 1), count INTEGER NOT NULL)",
      );
      await db.execute("INSERT OR IGNORE INTO counter (id, count) VALUES (1, 0)");
    },
  }),
  actions: {
    increment: async (c) => {
      await c.db.execute("UPDATE counter SET count = count + 1 WHERE id = 1");
      const [{ count }] = (await c.db.execute(
        "SELECT count FROM counter WHERE id = 1",
      )) as { count: number }[];
      c.broadcast("newCount", count);
      return count;
    },
    getCount: async (c) => {
      const [{ count }] = (await c.db.execute(
        "SELECT count FROM counter WHERE id = 1",
      )) as { count: number }[];
      return count;
    }
  }
});

// Create the registry
const registry = setup({
  use: { counter }
});

// Test data persistence
test("actor should persist data", async (testCtx) => {
  const { client } = await setupTest(testCtx, registry);
  const counterHandle = client.counter.getOrCreate(["test"]);

  // Initial value
  expect(await counterHandle.getCount()).toBe(0);

  // Modify the value
  await counterHandle.increment();

  // Verify the value 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({
  createVars: () => ({
    messages: [] as ChatMessage[]
  }),
  actions: {
    sendMessage: (c, username: string, message: string) => {
      c.vars.messages.push({ username, message });
      c.broadcast("newMessage", username, message);
    },
    getHistory: (c) => {
      return c.vars.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.getOrCreate(["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 by scheduling work and waiting for it to run:

import { expect, test } from "vitest";
import { actor, setup } from "rivetkit";
import { db } from "rivetkit/db";
import { setupTest } from "rivetkit/test";

const scheduler = actor({
  db: db({
    onMigrate: async (db) => {
      await db.execute(
        "CREATE TABLE IF NOT EXISTS completed_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
      );
    },
  }),
  actions: {
    scheduleTask: async (c, taskName: string) => {
      await c.schedule.after(50, "completeTask", taskName);
    },
    completeTask: async (c, taskName: string) => {
      await c.db.execute("INSERT INTO completed_tasks (name) VALUES (?)", taskName);
    },
    getCompletedTasks: async (c) => {
      const rows = (await c.db.execute(
        "SELECT name FROM completed_tasks ORDER BY id",
      )) as { name: string }[];
      return rows.map((row) => row.name);
    },
  },
});

const registry = setup({ use: { scheduler } });

test("scheduled work updates observable state", async (testCtx) => {
  const { client } = await setupTest(testCtx, registry);
  const handle = client.scheduler.getOrCreate(["test"]);

  await handle.scheduleTask("reminder");

  await expect
    .poll(() => handle.getCompletedTasks(), { timeout: 2_000, interval: 25 })
    .toContain("reminder");
});

Use a short schedule and expect.poll the action’s observable result. Vitest’s fake date and fake JavaScript timers do not advance RivetKit’s scheduler, which runs outside the test’s JavaScript timer queue.

Best Practices

  1. Isolate tests: Each test should run independently, avoiding shared state.
  2. Test edge cases: Verify how your actor handles invalid inputs, concurrent operations, and error conditions.
  3. Test scheduled operations: Poll observable state or output with a bounded timeout instead of sleeping for an exact duration.
  4. Use realistic data: Test with data that resembles production scenarios.

setupTest starts the registry and disposes the returned client when the test finishes, so you can focus on writing effective tests for your business logic.

Edit this page Last updated November 10, 2026