Best Practices
Design guidelines for Nostr client testing with tsunagiya.
Test Organization
Test Granularity
Good: One assertion per test
typescript
Deno.test("receives EOSE after sending REQ", async () => {/* ... */});
Deno.test("stored event matches filter", async () => {/* ... */});
Deno.test("unregistered URL closes with code:1006", async () => {/* ... */});Bad: Multiple assertions in one test
typescript
// ❌ This should be split
Deno.test("test all relay features", async () => {
// REQ → EVENT → CLOSE → AUTH → disconnect... all in one
});Test Naming Conventions
Recommended: Descriptive names
Use clear, descriptive test names that describe behavior.
typescript
// ✅ Good
Deno.test("kind:1 event matches filter", () => {});
Deno.test("new connection after refuse returns error", () => {});
Deno.test("processes 1000 events within 100ms", () => {});
// ❌ Bad
Deno.test("test1", () => {});
Deno.test("it works", () => {});Naming Patterns
| Pattern | Example |
|---|---|
[subject] [condition] [expectation] | filter matches kind:1 with one result |
[action] results in [outcome] | refuse() causes connection to be rejected |
when [situation], [behavior] | when URL is unregistered, connection fails |
Applying the DRY Principle
Test Execution Speed Optimization
1. Minimize latency
typescript
// ❌ Slow: simulates actual delay
pool.relay("wss://relay.example.com", { latency: 2000 });
// ✅ Fast: zero latency for non-latency tests
pool.relay("wss://relay.example.com"); // default is 0ms2. Set short timeouts
typescript
pool.relay("wss://relay.example.com", { connectionTimeout: 50 });3. Use short intervals for streamEvents
typescript
// ❌ Slow
streamEvents(relay, events, { interval: 1000 });
// ✅ Fast
streamEvents(relay, events, { interval: 10 });Always Use try/finally
Always wrap pool.install() and pool.uninstall() in try/finally.
Forgetting uninstall() leaves globalThis.WebSocket replaced, which will break subsequent tests.
Code Examples
Test file organization example:
tests/
├── relay/
│ ├── connection_test.ts # connection/disconnection
│ ├── req_test.ts # REQ/EOSE
│ ├── event_test.ts # EVENT/OK
│ └── auth_test.ts # NIP-42 AUTH
├── pool/
│ ├── multi_relay_test.ts # multiple relays
│ └── failover_test.ts # failover
├── client/
│ ├── timeline_test.ts # timeline fetching
│ ├── publish_test.ts # publishing
│ └── stream_test.ts # real-time
└── helpers/
└── setup.ts # common setupExtracting common setup:
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();
}
}Grouping tests:
typescript
Deno.test("MockRelay", async (t) => {
await t.step("can register events with store()", () => {/* ... */});
await t.step("can set custom handler with onREQ()", () => {/* ... */});
await t.step("can refuse connections with refuse()", () => {/* ... */});
});DRY principle — eliminate duplication with helper functions:
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;
}Clear state with pool.reset():
typescript
Deno.test("test suite", async (t) => {
const pool = new MockPool();
const relay = pool.relay("wss://relay.example.com");
pool.install();
try {
await t.step("test 1", async () => {
relay.store(EventBuilder.kind1().build());
// ... test ...
pool.reset();
});
await t.step("test 2", async () => {
// starts from clean state
});
} finally {
pool.uninstall();
}
});Leveraging concurrent tests:
typescript
Deno.test("concurrent tests", 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 pattern (required):
typescript
// ✅ Required pattern
pool.install();
try {
// tests
} finally {
pool.uninstall();
}Related Documentation
- Test Patterns — Test pattern collection
- Performance — Performance optimization
- Troubleshooting — Error resolution