ベストプラクティス
繋ぎ屋を使った Nostr クライアントテストの設計指針です。
テストの構成方法
テストの粒度
良い例:1テスト1検証
typescript
Deno.test("REQ送信後にEOSEを受信する", async () => {/* ... */});
Deno.test("storeしたイベントがフィルターにマッチする", async () => {/* ... */});
Deno.test("未登録URLへの接続はcode:1006で閉じる", async () => {/* ... */});悪い例:1テストで複数のことを検証
typescript
// ❌ これは分割すべき
Deno.test("リレーの全機能テスト", async () => {
// REQ → EVENT → CLOSE → AUTH → disconnect... 全部入り
});テストの命名規則
日本語での命名を推奨
繋ぎ屋は日本語プロジェクトなので、テスト名も日本語で書くと分かりやすいです。
typescript
// ✅ 良い例
Deno.test("kind:1のイベントがフィルターにマッチする", () => {});
Deno.test("接続拒否後の新規接続はエラーになる", () => {});
Deno.test("1000件のイベントを100ms以内に処理する", () => {});
// ❌ 悪い例
Deno.test("test1", () => {});
Deno.test("it works", () => {});命名パターン
| パターン | 例 |
|---|---|
[対象]が[条件]で[期待結果] | フィルターがkind:1で1件マッチする |
[操作]すると[結果] | refuse()すると接続が拒否される |
[状況]のとき[動作] | 未登録URLのとき接続失敗する |
DRY 原則の適用
テストの実行速度最適化
1. レイテンシを最小限にする
typescript
// ❌ 遅い:実際の遅延をシミュレート
pool.relay("wss://relay.example.com", { latency: 2000 });
// ✅ 速い:遅延テスト以外ではレイテンシ0
pool.relay("wss://relay.example.com"); // デフォルトは0ms2. タイムアウトを短くする
typescript
pool.relay("wss://relay.example.com", { connectionTimeout: 50 });3. streamEvents の間隔を短くする
typescript
// ❌ 遅い
streamEvents(relay, events, { interval: 1000 });
// ✅ 速い
streamEvents(relay, events, { interval: 10 });try/finally パターンの徹底
pool.install() と pool.uninstall() は必ず try/finally で囲むこと。
uninstall() を忘れると globalThis.WebSocket が差し替えられたままになり、後続のテストが壊れます。
コード例
テストファイルの構成例:
tests/
├── relay/
│ ├── connection_test.ts # 接続・切断
│ ├── req_test.ts # REQ/EOSE
│ ├── event_test.ts # EVENT/OK
│ └── auth_test.ts # NIP-42 AUTH
├── pool/
│ ├── multi_relay_test.ts # 複数リレー
│ └── failover_test.ts # フェイルオーバー
├── client/
│ ├── timeline_test.ts # タイムライン取得
│ ├── publish_test.ts # 投稿
│ └── stream_test.ts # リアルタイム
└── helpers/
└── setup.ts # 共通セットアップ共通セットアップの抽出:
typescript
// tests/helpers/setup.ts
import { MockPool } from "@ikuradon/tsunagiya";
export function createTestPool(urls: string[] = ["wss://relay.example.com"]) {
const pool = new MockPool();
const relays = urls.map((url) => pool.relay(url));
return { pool, relays, relay: relays[0] };
}
export async function withPool(
fn: (pool: MockPool) => Promise<void>,
urls?: string[],
) {
const { pool } = createTestPool(urls);
pool.install();
try {
await fn(pool);
} finally {
pool.uninstall();
}
}テストのグルーピング:
typescript
Deno.test("MockRelay", async (t) => {
await t.step("store() でイベントを登録できる", () => {/* ... */});
await t.step("onREQ() でカスタムハンドラーを設定できる", () => {/* ... */});
await t.step("refuse() で接続を拒否できる", () => {/* ... */});
});DRY 原則 — ヘルパー関数で重複を排除:
typescript
async function fetchEvents(
url: string,
filter: NostrFilter,
): Promise<NostrEvent[]> {
const events: NostrEvent[] = [];
const ws = new WebSocket(url);
await new Promise<void>((resolve) => {
ws.onopen = () => ws.send(JSON.stringify(["REQ", "s", filter]));
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg[0] === "EVENT") events.push(msg[2]);
if (msg[0] === "EOSE") ws.close();
};
ws.onclose = () => resolve();
});
return events;
}pool.reset() で状態をクリア:
typescript
Deno.test("テストスイート", async (t) => {
const pool = new MockPool();
const relay = pool.relay("wss://relay.example.com");
pool.install();
try {
await t.step("テスト1", async () => {
relay.store(EventBuilder.kind1().build());
// ... テスト ...
pool.reset();
});
await t.step("テスト2", async () => {
// クリーンな状態から開始
});
} finally {
pool.uninstall();
}
});並行テストの活用:
typescript
Deno.test("並行テスト", async () => {
const pool = new MockPool();
const relay1 = pool.relay("wss://test1.relay.test");
const relay2 = pool.relay("wss://test2.relay.test");
pool.install();
try {
await Promise.all([
testScenario1("wss://test1.relay.test"),
testScenario2("wss://test2.relay.test"),
]);
} finally {
pool.uninstall();
}
});try/finally パターン(必須):
typescript
// ✅ 必須パターン
pool.install();
try {
// テスト
} finally {
pool.uninstall();
}関連ドキュメント
- テストパターン — テストパターン集
- パフォーマンス — パフォーマンス最適化
- トラブルシューティング — エラー解決