Testing
plasma のテストスイートは完全に Node 上で動作します — ブラウザなし、Cloudflare エッジなし。あなたのアプリのテストも同じことができます。このガイドでは 2 つのハーネスを解説します。
クライアント側 — fake-indexeddb
Section titled “クライアント側 — fake-indexeddb”pnpm add -D fake-indexeddb vitestテストファイルの先頭で一度 import します:
import "fake-indexeddb/auto"それだけです — plasma のクライアントエンジンは polyfill された indexedDB、IDBKeyRange、IDBOpenDBRequest などを透過的に使います。
最小の mutator テスト
Section titled “最小の mutator テスト”import "fake-indexeddb/auto"import { createDb, defineSchema, eq, id, int, table, text } from "@sh1n4ps/plasma-core"import { createIdbEngine } from "@sh1n4ps/plasma-client"import { describe, expect, it } from "vitest"
const todos = table("todos", { id: id(), title: text(), done: int().default(0) })const schema = defineSchema({ todos })
async function makeDb() { const engine = await createIdbEngine({ schema, dbName: `test-${Math.random().toString(36).slice(2, 8)}`, }) return createDb({ schema, engine })}
describe("markDone mutator", () => { it("flips the done flag", async () => { const db = await makeDb() await db.insert(todos).values({ id: "t1", title: "hi" }) await db.update(todos).set({ done: 1 }).where(eq(todos.id, "t1"))
const rows = await db.select().from(todos) expect(rows[0]?.done).toBe(1) })})createIdbEngine はスタンドアロンのエンジンコンストラクタです — PlasmaClient の足場なし、sync ループなし。アプリが使うのと同じクエリビルダーに対して mutator ロジックを単体テストするのに最適です。
live query を決定的にテストする
Section titled “live query を決定的にテストする”IVM のセットアップは非同期です — subscribe(cb) は初回コールバックが発火する前に返ります。whenReady() を使ってください:
it("emits the initial snapshot", async () => { const db = await makeDb() await db.insert(todos).values({ id: "t1", title: "hi" })
const live = db.select().from(todos).live() const seen: number[] = [] live.subscribe((rows) => seen.push(rows.length))
await live.whenReady?.() expect(seen).toEqual([1])})whenReady() なしでは、setTimeout(0) の待機が忙しい CI マシン上で IDB のシード読み取りと競合します。
サーバー側 — Miniflare + @cloudflare/vitest-pool-workers
Section titled “サーバー側 — Miniflare + @cloudflare/vitest-pool-workers”Worker 側のテストには D1 エミュレータと workerd ランタイムが必要です。Cloudflare は両方を提供する @cloudflare/vitest-pool-workers を提供しています。
pnpm add -D @cloudflare/vitest-pool-workers wranglervitest.config.ts:
import { defineConfig } from "vitest/config"
export default defineConfig({ test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.jsonc" }, }, }, projects: [ { test: { name: "server", include: ["packages/server/test-workerd/**/*.test.ts"], poolOptions: { workers: { main: "./packages/server/test-workerd/entry.ts", miniflare: { d1Databases: { DB: "test-db" }, r2Buckets: { BUCKET: "test-bucket" }, }, }, }, }, }, ], },})wrangler.jsonc はテスト worker が使う D1 と R2 のバインディングを宣言します — Miniflare がそれらをインプロセスでエミュレートします。
データのシード
Section titled “データのシード”プロダクションで使うのと同じ ensureSchema を使います:
import { env } from "cloudflare:test"import { ensureSchema, fromD1 } from "@sh1n4ps/plasma-server"
beforeEach(async () => { const executor = fromD1(env.DB) await ensureSchema({ schema, executor })})env.DB は Miniflare の D1 エミュレータ、env.BUCKET は Miniflare の R2 エミュレータです。テスト worker のブートごとに空で始まります; 必要な table をシードしてください。
end-to-end の push/pull
Section titled “end-to-end の push/pull”実際の Request オブジェクトで sync ハンドラを駆動できます:
import { createSyncHandler } from "@sh1n4ps/plasma-server"
it("push then pull round-trips a mutation", async () => { const executor = fromD1(env.DB) await ensureSchema({ schema, executor })
const handler = createSyncHandler({ schema, mutators, executor, schemaVersion: "v1", auth: async () => ({ ok: true, clientGroupID: "g", clientID: "c", ctx: {} }), })
const pushRes = await handler(new Request("https://plasma.test/sync/push", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ protocolVersion: 1, schemaVersion: "v1", clientGroupID: "g", clientID: "c", mutations: [{ id: 1, name: "createTodo", args: { id: "t1", title: "hi", updatedAt: 0 } }], }), })) expect(pushRes.status).toBe(200)
const pullRes = await handler(new Request("https://plasma.test/sync/pull?protocolVersion=1&schemaVersion=v1&clientGroupID=g&clientID=c")) const body = await pullRes.json() expect(body.patch).toHaveLength(1)})これは auth、schema mismatch、envelope 検証、CRDT マージを含め、プロダクションが使うのと同じ sync-handler.ts のコードパスに対して実行されます。
PlasmaProvider を使った React コンポーネントのテスト
Section titled “PlasmaProvider を使った React コンポーネントのテスト”useLiveQuery / useMutation を使うコンポーネントテストには、PlasmaProvider でラップし、fake IDB でクライアントを構築します:
import { PlasmaProvider } from "@sh1n4ps/plasma-react"import { createPlasmaClient } from "@sh1n4ps/plasma-client"import { render } from "@testing-library/react"
const client = createPlasmaClient({ schema, mutators, dbName: `test-${Math.random()}`, endpoint: "https://plasma.test/sync", clientGroupID: "test", schemaVersion: "v1", getContext: async () => ({ userId: "test" }), fetcher: async () => new Response(JSON.stringify({ cookie: null, patch: [], lastMutationIDs: {}, hasMore: false, })), // すべての push/pull をスタブ化 pollIntervalMs: 60_000, retry: { maxAttempts: 1 },})
render( <PlasmaProvider client={client}> <TodoList /> </PlasmaProvider>,)fetcher スタブにより、実行中の Worker なしでクライアントをテストできます。push/pull の挙動をテストする必要があれば、レスポンスを手作りしてください。
決定的なタイミングのパターン
Section titled “決定的なタイミングのパターン”2 つのパターンが頻繁に登場します:
await live.whenReady?.()— 初回スナップショット配信をアサートする前。await new Promise(r => setTimeout(r, 0))— mutation をトリガーした後、リアクティブハブの同期的な emit が伝播し終わるのを待つため。
どちらのパターンもマジックナンバーを必要としません。whenReady は本物の Promise を待ち、emit にはゼロティックのマイクロタスクで十分です。
次に読むべきもの
Section titled “次に読むべきもの”- Concepts / Push, Pull, Rebase — 各 end-to-end テストが行使できるフェーズ
- Deployment — テストでエミュレートしているプロダクションの
wrangler.jsoncの形