# Testing Patterns

> Testing standards and patterns across TypeScript (Vitest), Rust (cargo test), Swift (Swift Testing), and Go (go test). Covers file conventions, mock strategies, async testing, CI integration, and anti-patterns based on real project usage.

- Skill: `0xmassi/testing-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 0xmassi/testing-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/0xmassi/testing-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: 0xMassi (https://skillmd.com/u/0xmassi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/0xmassi/testing-patterns

---


# Testing Patterns

## TypeScript / JavaScript Testing (Vitest)

### File Conventions
- **Colocated tests**: `src/foo.test.ts` next to `src/foo.ts` (primary pattern)
- **E2E tests**: `*.e2e.test.ts` in `test/` directory or colocated
- **Live tests**: `*.live.test.ts` for tests requiring real API keys
- **Setup file**: `test/setup.ts` for global beforeEach/afterEach hooks
- **Config layering**: base `vitest.config.ts` extended by `vitest.unit.config.ts`, `vitest.e2e.config.ts`, `vitest.live.config.ts`

### Test Structure
```typescript
import { describe, expect, it, vi } from "vitest";

describe("resolvePermission", () => {
  it("auto-approves safe tools without prompting", async () => {
    const prompt = vi.fn(async () => true);
    const res = await resolvePermission(makeRequest(), { prompt });
    expect(res).toEqual({ outcome: "allow" });
    expect(prompt).not.toHaveBeenCalled();
  });

  it("prompts for dangerous operations", async () => {
    const prompt = vi.fn(async () => true);
    await resolvePermission(makeDangerousRequest(), { prompt });
    expect(prompt).toHaveBeenCalledTimes(1);
    expect(prompt).toHaveBeenCalledWith("exec", "exec: rm -rf /");
  });
});
```

### Mock Patterns
```typescript
// Function mocks for dependency injection
const sendFn = vi.fn(async () => ({ messageId: "test" }));

// Module mocks (use sparingly -- prefer DI)
vi.mock("./network.js", () => ({
  fetch: vi.fn().mockResolvedValue({ ok: true }),
}));

// Spy on existing methods
const spy = vi.spyOn(store, "save");
expect(spy).toHaveBeenCalledWith(expectedData);

// Stub environment variables (auto-cleaned with unstubEnvs: true)
vi.stubEnv("API_KEY", "test-key-123");

// Fake timers (guard against leaks in afterEach)
vi.useFakeTimers();
vi.advanceTimersByTime(5000);
// Always restore: vi.useRealTimers()
```

### Test Setup Pattern (Global)
```typescript
// test/setup.ts
import { afterAll, afterEach, beforeEach, vi } from "vitest";
import { withIsolatedTestHome } from "./test-env.js";

process.env.VITEST = "true";
const testEnv = withIsolatedTestHome(); // Isolated HOME per worker
afterAll(() => testEnv.cleanup());

afterEach(() => {
  if (vi.isFakeTimers()) vi.useRealTimers(); // Prevent cross-test leaks
});
```

### Factory Functions for Test Data
```typescript
// Prefer factory functions over raw object literals
function makePermissionRequest(
  overrides: Partial<PermissionRequest> = {},
): PermissionRequest {
  const base: PermissionRequest = {
    sessionId: "session-1",
    toolCall: { toolCallId: "tool-1", title: "read: src/index.ts", status: "pending" },
    options: [{ kind: "allow_once", optionId: "allow" }],
  };
  return { ...base, ...overrides };
}
```

### Coverage Configuration
```typescript
// vitest.config.ts
coverage: {
  provider: "v8",
  reporter: ["text", "lcov"],
  all: false, // Only count files exercised by tests
  thresholds: { lines: 70, functions: 70, branches: 55, statements: 70 },
  include: ["./src/**/*.ts"],
  exclude: ["src/**/*.test.ts", "src/cli/**", "src/gateway/**"],
}
```

### Anti-Patterns
- Do NOT set `maxWorkers` above 16 (diminishing returns, flaky on CI)
- Do NOT test implementation details (internal state, private methods)
- Do NOT use `vi.mock` when dependency injection via function params works
- Do NOT leak fake timers across tests -- always restore in `afterEach`
- Do NOT skip edge cases: empty input, null, boundary values, error paths

---

## Rust Testing (cargo test)

### File Conventions
- **Inline unit tests**: `#[cfg(test)] mod tests` at the bottom of each source file (primary pattern)
- **Integration tests**: `tests/` directory at crate root (rare in workspace projects)
- **Test-only dependencies**: in `[dev-dependencies]` of each crate's `Cargo.toml`
- **Run all**: `cargo test --lib` | **Single crate**: `cargo test -p claudio-gateway --lib`
- **Filter by name**: `cargo test --lib test_name_substring`
- **Skip slow tests**: `cargo test --lib -- --skip concurrent`

### Unit Test Structure
```rust
#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;

    #[test]
    fn insert_and_get() {
        let map = BoundedMap::new(10, Duration::from_secs(60));
        assert!(map.insert("a", 1));
        assert_eq!(map.get("a"), Some(1));
    }

    #[test]
    fn capacity_enforced() {
        let map = BoundedMap::new(3, Duration::from_secs(60));
        map.insert("a", 1);
        map.insert("b", 2);
        map.insert("c", 3);
        map.insert("d", 4); // should evict one
        assert!(map.len() <= 3);
    }

    #[test]
    fn ttl_expiry_on_get() {
        let map = BoundedMap::new(10, Duration::from_millis(1));
        map.insert("a", 1);
        std::thread::sleep(Duration::from_millis(5));
        assert_eq!(map.get("a"), None);
    }
}
```

### Async Tests
```rust
#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    #[tokio::test]
    async fn returns_immediately_when_no_tool_calls() {
        let provider = Arc::new(MockToolProvider::new(vec![final_response("Hello!")]));
        let mut executor = AgentExecutor::new(provider, "http://unused:9999", 10);

        let result = executor.execute(test_request()).await.unwrap();
        assert_eq!(result.iterations, 1);
        assert_eq!(result.tool_calls_made, 0);
    }

    #[tokio::test]
    async fn max_iterations_produces_final_response() {
        // Provider always returns tool calls -- should hit max iterations
        let provider = Arc::new(MockToolProvider::new(vec![tool_call_response()]));
        let mut executor = AgentExecutor::new(provider, "http://unused:9999", 3);
        let result = executor.execute(test_request()).await.unwrap();
        assert!(result.iterations <= 3);
    }
}
```

### Stub/Mock Providers (Trait-Based Mocking)
```rust
/// Stub: always returns a fixed response. No network calls.
pub struct StubProvider { provider_name: String }

#[async_trait]
impl Provider for StubProvider {
    fn name(&self) -> &str { &self.provider_name }
    async fn chat_completion(&self, req: &ChatCompletionRequest)
        -> Result<ChatCompletionResponse, ProviderError> {
        Ok(ChatCompletionResponse { /* canned fields */ })
    }
}

/// Scripted: returns pre-configured responses in sequence.
/// Useful for multi-turn agent loops.
pub struct ScriptedProvider {
    responses: std::sync::Mutex<Vec<ChatCompletionResponse>>,
}
```

### Testing Patterns for Workspace Projects
- Each crate has its own `#[cfg(test)]` modules (typical: 100+ test files in a workspace)
- Shared test utilities live in dedicated stub modules (e.g., `providers/src/stub.rs`)
- Use `Arc<dyn Trait>` for mock injection in async contexts
- Test crates independently: `cargo test -p crate-name --lib`

### Tauri Command Testing
```rust
// Minimal inline tests for Tauri commands
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn is_newer_detects_version_bump() {
        assert!(is_newer("1.0.1", "1.0.0"));
        assert!(!is_newer("1.0.0", "1.0.0"));
        assert!(!is_newer("0.9.9", "1.0.0"));
    }
}
```

---

## Swift / iOS Testing (Swift Testing)

### Framework: Swift Testing (not XCTest)
Projects use the modern `Testing` framework with `@Test` macros and `#expect` assertions.

### Test Structure
```swift
import Foundation
import Testing
@testable import YourApp

@Suite struct KeychainStoreTests {
    @Test func saveLoadUpdateDeleteRoundTrip() {
        let service = "com.example.tests.\(UUID().uuidString)"
        let account = "value"

        #expect(KeychainStore.delete(service: service, account: account))
        #expect(KeychainStore.loadString(service: service, account: account) == nil)
        #expect(KeychainStore.saveString("first", service: service, account: account))
        #expect(KeychainStore.loadString(service: service, account: account) == "first")
    }
}
```

### Serialized Suites (Shared State)
```swift
@Suite(.serialized) struct GatewayConnectionControllerTests {
    @Test @MainActor func resolvedDisplayNameSetsDefaultWhenMissing() {
        withUserDefaults([displayKey: nil, "node.instanceId": "ios-test"]) {
            let appModel = NodeAppModel()
            let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false)
            let resolved = controller._test_resolvedDisplayName(defaults: .standard)
            #expect(!resolved.isEmpty)
        }
    }
}
```

### Async Test Methods
```swift
@Suite(.serialized) struct VoiceWakeManagerStateTests {
    @Test @MainActor func suspendAndResumeCycleUpdatesState() async {
        let manager = VoiceWakeManager()
        manager.isEnabled = true
        manager.isListening = true

        let suspended = manager.suspendForExternalAudioCapture()
        #expect(suspended == true)
        #expect(manager.isListening == false)

        manager.resumeAfterExternalAudioCapture(wasSuspended: true)
        try? await Task.sleep(nanoseconds: 900_000_000)
        #expect(manager.statusText.contains("Voice Wake") == true)
    }
}
```

### Testing @Observable ViewModels
- Instantiate the `@Observable` class directly in the test
- Use `@MainActor` annotation for UI-bound state
- For UserDefaults-dependent code, use a `withUserDefaults` helper that snapshots and restores

### Test Helper Pattern: Expose Internals
```swift
// In production code, expose test-only accessors:
func _test_resolvedDisplayName(defaults: UserDefaults) -> String { ... }
func _test_currentCaps() -> [String] { ... }
func _test_handleRecognitionCallback(transcript: String?, ...) { ... }
```

### Actor-Based Test Capture
```swift
actor CaptureBox {
    var value: String?
    func set(_ next: String) { self.value = next }
}

let capture = CaptureBox()
manager.configure { cmd in await capture.set(cmd) }
```

### Build & Test Commands
```bash
# Run tests on simulator
xcodebuild test -project App.xcodeproj -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro'

# Build only (catches compile errors)
xcodebuild -project App.xcodeproj -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
```

---

## Go Testing (go test)

### File Conventions
- Test files: `*_test.go` in the same package as the code under test
- Same package access: tests use `package crypto` (not `package crypto_test`) for internal access
- Run: `go test ./...` | Single package: `go test ./internal/crypto/`

### Standard Test Pattern
```go
package crypto

import (
    "bytes"
    "testing"
)

func TestEncryptDecrypt(t *testing.T) {
    key := make([]byte, chacha20poly1305.KeySize)
    rand.Read(key)
    plaintext := []byte("Hello, World!")

    encrypted, err := Encrypt(plaintext, key, nil)
    if err != nil {
        t.Fatalf("Encrypt failed: %v", err)
    }

    decrypted, err := Decrypt(encrypted, key, nil)
    if err != nil {
        t.Fatalf("Decrypt failed: %v", err)
    }

    if !bytes.Equal(decrypted, plaintext) {
        t.Errorf("Decrypted text doesn't match.\nGot:  %q\nWant: %q", decrypted, plaintext)
    }
}
```

### Error Case Testing
```go
func TestEncryptInvalidKey(t *testing.T) {
    shortKey := make([]byte, 16)
    _, err := Encrypt([]byte("test"), shortKey, nil)
    if err == nil {
        t.Error("Encrypt should reject short key")
    }
}
```

### Integration Tests with Channels
```go
func TestHandshakeClientServer(t *testing.T) {
    serverResult := make(chan *HandshakeResult, 1)
    serverErr := make(chan error, 1)

    server := network.NewServer(handler)
    if err := server.Listen("127.0.0.1:0"); err != nil {
        t.Fatalf("Failed to listen: %v", err)
    }
    go server.Serve()
    defer server.Close()

    conn, err := network.Dial(server.Addr().String())
    if err != nil {
        t.Fatalf("Failed to dial: %v", err)
    }
    defer conn.Close()

    select {
    case err := <-serverErr:
        t.Fatalf("Server handshake failed: %v", err)
    case result := <-serverResult:
        if result.PeerID != "test-client" {
            t.Errorf("Server got peer ID = %q, want %q", result.PeerID, "test-client")
        }
    case <-time.After(2 * time.Second):
        t.Fatal("Server handshake timeout")
    }
}
```

### Uniqueness / Determinism Tests
```go
func TestGenerateKeyUniqueness(t *testing.T) {
    k1, _ := GenerateIdentityKey()
    k2, _ := GenerateIdentityKey()
    if bytes.Equal(k1.PublicKey, k2.PublicKey) {
        t.Error("Generated identical public keys (should be unique)")
    }
}

func TestFingerprintDeterministic(t *testing.T) {
    keypair, _ := GenerateIdentityKey()
    f1 := keypair.Fingerprint()
    f2 := keypair.Fingerprint()
    if f1 != f2 {
        t.Error("Fingerprint is not deterministic")
    }
}
```

### Race Detection
```bash
go test -race ./...   # Always run in CI
```

---

## Cross-Language Guidelines

### When to Write Tests
- **Always test**: boundary logic, parsing, serialization, crypto, state machines, error paths
- **Always test**: bug fixes (write the failing test first)
- **Skip testing**: trivial getters/setters, framework wiring, one-line delegation
- **Skip testing**: third-party library behavior (that is their job)

### Test Naming Conventions
| Language   | Convention                                    | Example                                    |
|------------|-----------------------------------------------|--------------------------------------------|
| TypeScript | `describe("module") > it("does X when Y")`   | `it("auto-approves safe tools")`           |
| Rust       | `snake_case` function names                   | `fn insert_and_get()`                      |
| Swift      | `camelCase` method names on `@Suite` struct    | `func saveLoadUpdateDeleteRoundTrip()`     |
| Go         | `TestXxx` with `PascalCase`                   | `func TestEncryptDecrypt(t *testing.T)`    |

### The Testing Pyramid
```
        /  E2E  \        *.e2e.test.ts, Docker scripts
       / Integration \   Server tests, handshake tests
      /    Unit Tests  \  #[test], @Test, TestXxx, it()
```
Ratio target: ~70% unit, ~20% integration, ~10% e2e.

### What NOT to Test
- Framework internals (Tauri IPC plumbing, axum routing wiring, SwiftUI view layout)
- Third-party API responses (mock them instead)
- Trivial wrappers that just delegate to another function
- Generated code (protocol buffers, codegen output)

### CI Integration (GitHub Actions)

**TypeScript (Vitest)**:
```yaml
- name: Test
  run: pnpm test
- name: Coverage
  run: pnpm test:coverage
```

**Rust (cargo test)**:
```yaml
- name: Test
  run: cargo test --lib
- name: Clippy
  run: cargo clippy -- -D warnings
```

**Swift (xcodebuild)**:
```yaml
- name: Test
  run: |
    xcodebuild test -project App.xcodeproj -scheme App \
      -destination 'platform=iOS Simulator,name=iPhone 17 Pro'
```

**Go (go test)**:
```yaml
- name: Test
  run: go test -race -cover ./...
```

---

## Quick Reference

| Language   | Framework       | Run Command                          | Config File             | Convention              |
|------------|-----------------|--------------------------------------|-------------------------|-------------------------|
| TypeScript | Vitest 4.x      | `pnpm test` / `vitest run`          | `vitest.config.ts`      | `*.test.ts` colocated   |
| Rust       | cargo test       | `cargo test --lib`                   | `Cargo.toml` (workspace)| `#[cfg(test)] mod tests`|
| Swift      | Swift Testing    | `xcodebuild test`                    | `.xcodeproj` scheme     | `*Tests.swift` in Tests/|
| Go         | go test          | `go test ./...`                      | None (built-in)         | `*_test.go` same pkg    |

### Mock Strategy Per Language
| Language   | Primary Mock Approach                        | Avoid                              |
|------------|----------------------------------------------|------------------------------------|
| TypeScript | `vi.fn()` + dependency injection via params  | Heavy `vi.mock()` of whole modules |
| Rust       | Trait-based stubs (`StubProvider`)            | `mockall` for simple cases         |
| Swift      | `_test_` accessor methods + actor captures   | Swizzling, runtime patching        |
| Go         | Interface-based injection + channels         | Monkey patching, `reflect`         |

## Property-Based Testing

Use when the function has invariants you can express as a rule (commutativity, round-trip, idempotency, monotonicity). One property test replaces dozens of example cases.

| Language   | Library     | Quick example |
|------------|-------------|---------------|
| TypeScript | `fast-check` | `fc.assert(fc.property(fc.string(), s => decode(encode(s)) === s))` |
| Rust       | `proptest`   | `proptest! { #[test] fn round_trip(s in any::<String>()) { prop_assert_eq!(decode(&encode(&s)), s); } }` |
| Swift      | `swift-testing` + manual generators | Loop with `Int.random(in:)` over `#expect` |
| Go         | `testing/quick` (stdlib) or `gopter` | `quick.Check(func(s string) bool { return decode(encode(s)) == s }, nil)` |

Sweet spots: parsers, codecs, serialization, math utilities, sort/merge logic. Skip for pure I/O wrappers.

## Coverage Targets

| Layer                  | Target  |
|------------------------|---------|
| Pure logic, parsers    | 90%+    |
| Service / handler code | 70-80%  |
| UI / view layer        | 40-60%  |
| Glue, framework wiring | not measured |

Coverage is a floor, not a goal. 100% line coverage with no assertions on outputs proves nothing.

