Testing Deno KV: Patterns for Ephemeral and Persistent State (2026)

Testing Deno KV: Patterns for Ephemeral and Persistent State (2026)

Deno KV is a key-value store built into the Deno runtime. In development, it uses SQLite. In Deno Deploy, it uses FoundationDB. Testing KV code correctly means understanding both the API and how to isolate tests from each other.

This guide covers testing Deno KV: CRUD operations, atomic transactions, queues, watch streams, and patterns for keeping tests fast and isolated.

Opening a KV Store for Tests

For tests, always open an in-memory (ephemeral) store. Never use a persistent file path in tests — tests that share state are hard to debug and fail in non-obvious ways.

// Use ":memory:" for ephemeral in-process storage
const kv = await Deno.openKv(":memory:");

// Or open a temp file that auto-deletes
const tmpFile = await Deno.makeTempFile({ suffix: ".db" });
const kv = await Deno.openKv(tmpFile);
// Remember to delete tmpFile in cleanup

The ":memory:" option gives you a fresh store for each test. It's fast (no disk I/O) and completely isolated.

Testing Basic CRUD Operations

// user_store.ts
export interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

export class UserStore {
  constructor(private kv: Deno.Kv) {}

  async set(user: User): Promise<void> {
    await this.kv.set(["users", user.id], user);
    await this.kv.set(["users_by_email", user.email], user.id);
  }

  async getById(id: string): Promise<User | null> {
    const entry = await this.kv.get<User>(["users", id]);
    return entry.value;
  }

  async getByEmail(email: string): Promise<User | null> {
    const idEntry = await this.kv.get<string>(["users_by_email", email]);
    if (!idEntry.value) return null;
    return this.getById(idEntry.value);
  }

  async delete(id: string): Promise<void> {
    const user = await this.getById(id);
    if (!user) return;

    await this.kv.atomic()
      .delete(["users", id])
      .delete(["users_by_email", user.email])
      .commit();
  }

  async list(): Promise<User[]> {
    const entries = this.kv.list<User>({ prefix: ["users"] });
    const users: User[] = [];
    for await (const entry of entries) {
      users.push(entry.value);
    }
    return users;
  }
}
// user_store_test.ts
import { assertEquals, assertExists, assertEquals as assertNull } from "jsr:@std/assert";
import { UserStore, type User } from "./user_store.ts";

function makeUser(overrides: Partial<User> = {}): User {
  return {
    id: crypto.randomUUID(),
    name: "Ada Lovelace",
    email: "ada@example.com",
    createdAt: new Date(),
    ...overrides,
  };
}

Deno.test("UserStore - CRUD operations", async (t) => {
  const kv = await Deno.openKv(":memory:");
  const store = new UserStore(kv);

  try {
    await t.step("set and retrieve user by ID", async () => {
      const user = makeUser({ id: "user-1" });
      await store.set(user);

      const retrieved = await store.getById("user-1");
      assertExists(retrieved);
      assertEquals(retrieved.name, "Ada Lovelace");
    });

    await t.step("retrieve user by email", async () => {
      const user = makeUser({ id: "user-2", email: "grace@example.com" });
      await store.set(user);

      const retrieved = await store.getByEmail("grace@example.com");
      assertExists(retrieved);
      assertEquals(retrieved.id, "user-2");
    });

    await t.step("returns null for unknown ID", async () => {
      const retrieved = await store.getById("nonexistent-id");
      assertEquals(retrieved, null);
    });

    await t.step("returns null for unknown email", async () => {
      const retrieved = await store.getByEmail("nobody@example.com");
      assertEquals(retrieved, null);
    });

    await t.step("delete removes user and email index", async () => {
      const user = makeUser({ id: "user-3", email: "to-delete@example.com" });
      await store.set(user);

      await store.delete("user-3");

      assertEquals(await store.getById("user-3"), null);
      assertEquals(await store.getByEmail("to-delete@example.com"), null);
    });

    await t.step("list returns all stored users", async () => {
      // Start fresh
      const freshKv = await Deno.openKv(":memory:");
      const freshStore = new UserStore(freshKv);

      await freshStore.set(makeUser({ id: "a", email: "a@test.com" }));
      await freshStore.set(makeUser({ id: "b", email: "b@test.com" }));
      await freshStore.set(makeUser({ id: "c", email: "c@test.com" }));

      const users = await freshStore.list();
      assertEquals(users.length, 3);

      freshKv.close();
    });
  } finally {
    kv.close();
  }
});

Testing Atomic Transactions

Deno KV's atomic transactions commit all operations or none. Test both the success case and the check failure case.

// inventory.ts
export class Inventory {
  constructor(private kv: Deno.Kv) {}

  async addStock(productId: string, quantity: number): Promise<void> {
    const key = ["inventory", productId];
    const current = await this.kv.get<number>(key);
    const currentQty = current.value ?? 0;

    const result = await this.kv.atomic()
      .check(current) // optimistic lock: fails if value changed
      .set(key, currentQty + quantity)
      .commit();

    if (!result.ok) {
      throw new Error("Concurrent update detected — retry required");
    }
  }

  async reserveStock(productId: string, quantity: number): Promise<boolean> {
    const key = ["inventory", productId];
    const current = await this.kv.get<number>(key);
    const currentQty = current.value ?? 0;

    if (currentQty < quantity) return false;

    const result = await this.kv.atomic()
      .check(current)
      .set(key, currentQty - quantity)
      .commit();

    return result.ok;
  }

  async getStock(productId: string): Promise<number> {
    const entry = await this.kv.get<number>(["inventory", productId]);
    return entry.value ?? 0;
  }
}
// inventory_test.ts
import { assertEquals } from "jsr:@std/assert";
import { Inventory } from "./inventory.ts";

Deno.test("Inventory - atomic operations", async (t) => {
  const kv = await Deno.openKv(":memory:");
  const inventory = new Inventory(kv);

  try {
    await t.step("addStock increases quantity", async () => {
      await inventory.addStock("prod-1", 100);
      assertEquals(await inventory.getStock("prod-1"), 100);
    });

    await t.step("addStock is additive", async () => {
      await inventory.addStock("prod-2", 50);
      await inventory.addStock("prod-2", 30);
      assertEquals(await inventory.getStock("prod-2"), 80);
    });

    await t.step("reserveStock succeeds when stock is available", async () => {
      await inventory.addStock("prod-3", 10);
      const reserved = await inventory.reserveStock("prod-3", 3);

      assertEquals(reserved, true);
      assertEquals(await inventory.getStock("prod-3"), 7);
    });

    await t.step("reserveStock fails when insufficient stock", async () => {
      await inventory.addStock("prod-4", 2);
      const reserved = await inventory.reserveStock("prod-4", 5);

      assertEquals(reserved, false);
      assertEquals(await inventory.getStock("prod-4"), 2);
    });

    await t.step("reserveStock returns false for empty inventory", async () => {
      const reserved = await inventory.reserveStock("nonexistent-product", 1);
      assertEquals(reserved, false);
    });
  } finally {
    kv.close();
  }
});

Testing KV Queue Listeners

Deno KV queues deliver messages to listener functions. Testing them requires triggering an enqueue and verifying the listener processes it.

// email_queue.ts
export interface EmailJob {
  to: string;
  subject: string;
  body: string;
}

export async function enqueueEmail(kv: Deno.Kv, job: EmailJob): Promise<void> {
  await kv.enqueue(job, { delay: 0 });
}

export function startEmailWorker(
  kv: Deno.Kv,
  sendEmail: (job: EmailJob) => Promise<void>
): void {
  kv.listenQueue(async (job: EmailJob) => {
    await sendEmail(job);
  });
}
// email_queue_test.ts
import { assertEquals } from "jsr:@std/assert";
import { spy, assertSpyCalls, assertSpyCall } from "jsr:@std/mock";
import { enqueueEmail, startEmailWorker, type EmailJob } from "./email_queue.ts";

Deno.test({
  name: "email queue delivers job to listener",
  permissions: { net: false },
  async fn() {
    const kv = await Deno.openKv(":memory:");
    const sendEmail = spy(async (_job: EmailJob) => {});
    
    startEmailWorker(kv, sendEmail);

    await enqueueEmail(kv, {
      to: "ada@example.com",
      subject: "Welcome",
      body: "Welcome to our platform!",
    });

    // Give the queue time to process
    await new Promise((resolve) => setTimeout(resolve, 100));

    assertSpyCalls(sendEmail, 1);
    assertSpyCall(sendEmail, 0, {
      args: [{
        to: "ada@example.com",
        subject: "Welcome",
        body: "Welcome to our platform!",
      }],
    });

    kv.close();
  },
});

Testing Watch/Stream

Deno KV's watch method streams value changes in real time. Test reactive behavior by watching a key and verifying it updates:

// presence.ts
export class PresenceTracker {
  constructor(private kv: Deno.Kv) {}

  async setOnline(userId: string): Promise<void> {
    await this.kv.set(["presence", userId], { online: true, since: Date.now() });
  }

  async setOffline(userId: string): Promise<void> {
    await this.kv.delete(["presence", userId]);
  }

  watchUser(userId: string): ReadableStream<boolean> {
    const watchStream = this.kv.watch<{ online: boolean }[]>([["presence", userId]]);
    return watchStream.pipeThrough(
      new TransformStream({
        transform(entries, controller) {
          controller.enqueue(entries[0].value?.online ?? false);
        },
      })
    );
  }
}
// presence_test.ts
import { assertEquals } from "jsr:@std/assert";
import { PresenceTracker } from "./presence.ts";

Deno.test("PresenceTracker emits false when user goes offline", async () => {
  const kv = await Deno.openKv(":memory:");
  const tracker = new PresenceTracker(kv);

  // Set user online first
  await tracker.setOnline("user-1");

  const watchStream = tracker.watchUser("user-1");
  const reader = watchStream.getReader();

  // Take the first emission
  const firstRead = await reader.read();
  // Skip the initial value (true, since we just set online)

  // Set offline and capture the change
  await tracker.setOffline("user-1");

  const nextRead = await reader.read();
  assertEquals(nextRead.value, false);

  reader.releaseLock();
  kv.close();
});

Test Isolation Patterns

The key to reliable KV tests is isolation. Each test should start with a clean store.

Pattern 1: Memory store per test (fastest)

Deno.test("test name", async () => {
  const kv = await Deno.openKv(":memory:");
  const store = new MyStore(kv);

  try {
    // test code
  } finally {
    kv.close();
  }
});

Pattern 2: Unique key prefix per test (useful for stateful tests)

function testPrefix(): Deno.KvKey {
  return [crypto.randomUUID()];
}

Deno.test("isolated test with prefixed keys", async () => {
  const kv = await Deno.openKv(":memory:");
  const prefix = testPrefix();
  
  // All operations scoped to unique prefix
  await kv.set([...prefix, "key"], "value");
  const entry = await kv.get([...prefix, "key"]);
  
  assertEquals(entry.value, "value");
  kv.close();
});

Pattern 3: Shared KV with cleanup (for integration tests with setup cost)

const sharedKv = await Deno.openKv(":memory:");

async function clearAll() {
  const entries = sharedKv.list({ prefix: [] });
  for await (const entry of entries) {
    await sharedKv.delete(entry.key);
  }
}

Deno.test("first test", async () => {
  await clearAll();
  // test using sharedKv
});

Deno.test("second test", async () => {
  await clearAll();
  // test using sharedKv
});

Production Monitoring with HelpMeTest

Deno KV in production runs on FoundationDB (via Deno Deploy), which is different from the local SQLite used in development. Behaviors that differ:

  • Eventual consistency in cross-region deployments
  • Different latency characteristics for atomic operations
  • Queue delivery timing under load

HelpMeTest monitors your Deno KV-backed app in production:

Go to https://myapp.deno.dev/dashboard
Click "Add item"
Fill in item name with "Test Item"
Click Save
Verify "Test Item" appears in the list

Free tier: 10 tests, 5-minute check intervals.
Pro: $100/month
— unlimited tests, 24/7 monitoring.


Start free at helpmetest.com — no credit card required.

Read more