npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@douglance/stdb-test-utils

v1.0.0

Published

Enable fast, isolated unit testing of SpacetimeDB reducer and system logic without live database

Readme

@spacetimedb/test-utils

Enable fast, isolated unit testing of SpacetimeDB reducer and system logic without requiring a live database.

Installation

npm install --save-dev @spacetimedb/test-utils
# or
bun add --dev @spacetimedb/test-utils

Problem Solved

Testing SpacetimeDB reducers traditionally requires:

  • Deploying to a live SpacetimeDB instance
  • Complex setup and teardown
  • Slow feedback loops

@spacetimedb/test-utils provides a lightweight mock environment for testing reducer logic in isolation with fast, synchronous tests.

Features

  • Mock Database: In-memory table operations without SpacetimeDB runtime
  • Mock Context: Simulated ReducerContext with sender, timestamp, and database access
  • Type-Safe: Full TypeScript support with generics
  • Fast: Synchronous, in-process testing
  • Bun Native: Uses Bun's built-in mock functions for assertions

Usage

Basic Example

import { describe, test, expect } from "bun:test";
import { createTestContext } from "@spacetimedb/test-utils";
import type { GameSchema } from "./generated/schema";
import { handleInput } from "./reducers/input";

describe("handleInput reducer", () => {
  test("updates player input correctly", () => {
    const { callReducer, db, setSender } = createTestContext(GameSchema);

    // Setup: Create test data
    const playerId = Identity.fromString("0x01");
    db.playerInput.data.set(playerId.toHexString(), {
      entity_id: playerId.toHexString(),
      up: false,
      down: false,
      left: false,
      right: false,
    });

    // Set the sender identity
    setSender(playerId);

    // Execute: Call the reducer
    callReducer(handleInput, {
      entity_id: playerId.toHexString(),
      up: true,
      down: false,
      left: true,
      right: false,
    });

    // Assert: Verify state changes
    const updatedPlayer = db.playerInput.data.get(playerId.toHexString());
    expect(updatedPlayer?.up).toBe(true);
    expect(updatedPlayer?.left).toBe(true);
    expect(updatedPlayer?.down).toBe(false);
    expect(updatedPlayer?.right).toBe(false);

    // Assert: Verify mock was called
    expect(db.playerInput.update).toHaveBeenCalledTimes(1);
  });
});

Testing Insert Operations

test("join_game creates player entity", () => {
  const { callReducer, db } = createTestContext(GameSchema);

  callReducer(joinGame, { name: "Alice" });

  expect(db.Entity.data.size).toBe(1);
  expect(db.Position.data.size).toBe(1);
  expect(db.PlayerTag.data.size).toBe(1);

  const [entity] = Array.from(db.Entity.data.values());
  expect(entity).toBeDefined();

  expect(db.Entity.insert).toHaveBeenCalledTimes(1);
  expect(db.Position.insert).toHaveBeenCalledTimes(1);
  expect(db.PlayerTag.insert).toHaveBeenCalledTimes(1);
});

Testing with Custom Sender

test("only owner can call admin reducer", () => {
  const { callReducer, setSender, ctx } = createTestContext(GameSchema);

  const adminId = Identity.fromString("0xADMIN");
  const userId = Identity.fromString("0xUSER");

  // Setup: Add admin to owner table
  db.Owner.data.set(adminId.toHexString(), { id: adminId.toHexString() });

  // Test 1: Admin can call
  setSender(adminId);
  expect(() => callReducer(resetGame, {})).not.toThrow();

  // Test 2: Regular user cannot call
  setSender(userId);
  expect(() => callReducer(resetGame, {})).toThrow("Not authorized");
});

Testing Physics Systems

test("physics system updates positions from velocity", () => {
  const { callReducer, db } = createTestContext(GameSchema);

  // Setup: Create entity with position and velocity
  const entityId = Identity.fromString("0x01");
  db.Position.data.set(entityId.toHexString(), {
    entity_id: entityId.toHexString(),
    x: 100,
    y: 100,
  });
  db.Velocity.data.set(entityId.toHexString(), {
    entity_id: entityId.toHexString(),
    dx: 10,
    dy: 20,
  });

  // Execute: Run physics tick
  callReducer(physicsTick, { dt: 1.0 });

  // Assert: Position updated by velocity
  const pos = db.Position.data.get(entityId.toHexString());
  expect(pos?.x).toBe(110);
  expect(pos?.y).toBe(120);
});

API Reference

createTestContext<S>(schema: S): TestContext<S>

Creates a test context with mock database and reducer context.

Parameters:

  • schema: SpacetimeDB schema definition (usually exported from generated code)

Returns: TestContext<S> with:

  • ctx: Mock ReducerContext with sender, timestamp, db access
  • db: Mock database with all tables from schema
  • setSender(identity: Identity): Change the sender identity
  • callReducer(fn, args): Execute a reducer function with args

MockTableOperations<TRow>

Each table in db provides:

  • data: Map<any, TRow> - Direct access to table rows
  • insert(row: TRow): Mock function to insert row
  • update(row: TRow): Mock function to update row
  • delete(primaryKey: any): Mock function to delete row
  • find(primaryKey: any): Mock function to find row by key
  • iter(): Mock function returning iterator over all rows

All mock functions support Bun's assertion matchers:

  • expect(mockFn).toHaveBeenCalledWith(...args)
  • expect(mockFn).toHaveBeenCalledTimes(n)

Testing Best Practices

1. Test Reducer Logic in Isolation

Focus on testing the business logic without SpacetimeDB runtime overhead:

test("damage reduces health", () => {
  const { callReducer, db } = createTestContext(GameSchema);

  db.Health.data.set("player1", { entity_id: "player1", hp: 100 });

  callReducer(applyDamage, { entity_id: "player1", damage: 30 });

  expect(db.Health.data.get("player1")?.hp).toBe(70);
});

2. Verify Table Operations

Assert that reducers call the correct database operations:

test("leave_game removes all player components", () => {
  const { callReducer, db } = createTestContext(GameSchema);

  // Setup state
  const playerId = "player1";
  db.Entity.data.set(playerId, { id: playerId });
  db.Position.data.set(playerId, { entity_id: playerId, x: 0, y: 0 });
  db.Velocity.data.set(playerId, { entity_id: playerId, dx: 0, dy: 0 });

  callReducer(leaveGame, { entity_id: playerId });

  // Verify deletes were called
  expect(db.Entity.delete).toHaveBeenCalledWith(playerId);
  expect(db.Position.delete).toHaveBeenCalledWith(playerId);
  expect(db.Velocity.delete).toHaveBeenCalledWith(playerId);

  // Verify state is cleaned up
  expect(db.Entity.data.has(playerId)).toBe(false);
});

3. Test Edge Cases

Cover error conditions and boundary cases:

test("handleInput ignores input for non-existent player", () => {
  const { callReducer, db } = createTestContext(GameSchema);

  callReducer(handleInput, {
    entity_id: "nonexistent",
    up: true,
    down: false,
    left: false,
    right: false,
  });

  expect(db.PlayerInput.update).not.toHaveBeenCalled();
});

4. Use beforeEach for Setup

Keep tests isolated with fresh state:

describe("combat system", () => {
  let testCtx: TestContext<typeof GameSchema>;

  beforeEach(() => {
    testCtx = createTestContext(GameSchema);

    // Common setup
    testCtx.db.Entity.data.set("player1", { id: "player1" });
    testCtx.db.Health.data.set("player1", { entity_id: "player1", hp: 100 });
  });

  test("attack reduces target health", () => {
    testCtx.callReducer(attack, { attacker: "player1", target: "enemy1", damage: 25 });
    // ...
  });
});

Limitations

  • Does not test SpacetimeDB-specific features (indexes, scheduled reducers, client subscriptions)
  • Does not test network behavior or client-server interactions
  • Does not validate schema constraints (use SpacetimeDB's schema validation for that)

For integration testing with real SpacetimeDB runtime, use spacetime publish with test database.

License

MIT

Contributing

Issues and PRs welcome at the project repository.