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 cleanupThe ":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 listFree tier: 10 tests, 5-minute check intervals.
Pro: $100/month — unlimited tests, 24/7 monitoring.
Start free at helpmetest.com — no credit card required.