home / skills / rshankras / claude-code-apple-skills / test-contract

test-contract skill

/skills/testing/test-contract

This skill defines protocol contract tests in Swift to ensure every implementation passes identical behavioral tests.

npx playbooks add skill rshankras/claude-code-apple-skills --skill test-contract

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
9.0 KB
---
name: test-contract
description: Generate protocol/interface test suites that any implementation must pass. Define the contract once, test every implementation. Use when designing protocols or swapping implementations.
allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion]
---

# Test Contract

Define behavioral contracts as test suites. Any class conforming to a protocol must pass the contract tests. Ensures consistent behavior across implementations — human-written, AI-generated, or mock.

## When This Skill Activates

Use this skill when the user:
- Designs a protocol and wants to ensure conformance quality
- Has multiple implementations of the same interface (e.g., real + mock + in-memory)
- Says "test the protocol" or "contract test"
- Wants to swap implementations safely (e.g., CoreData → SwiftData)
- Wants to ensure AI-generated implementations meet the spec

## Why Contract Tests

```
Protocol: DataStore
  ├── SQLiteDataStore  → must pass contract tests
  ├── InMemoryDataStore → must pass contract tests
  ├── MockDataStore    → must pass contract tests
  └── CloudDataStore   → must pass contract tests
```

Write the contract once. Every implementation proves it works by passing the same tests.

## Process

### Phase 1: Identify the Protocol

```
Grep: "protocol.*\\{" to find protocols
Read: protocol definition
```

Understand:
- [ ] All required methods and properties
- [ ] Expected behavior for each method
- [ ] Error conditions and edge cases
- [ ] Thread safety requirements
- [ ] State invariants (e.g., count matches items.count)

### Phase 2: Define Contract Behaviors

For each protocol method, define required behaviors:

```markdown
## DataStore Contract

### save(_ item:)
- Saving a new item increases count by 1
- Saving an existing item (same ID) updates it, count unchanged
- Saved item is retrievable by ID
- Throws on invalid item (empty title)

### fetch(id:)
- Returns item when it exists
- Returns nil when item doesn't exist
- Returns most recently saved version

### delete(id:)
- Deleting existing item decreases count by 1
- Deleting non-existent item does nothing (no throw)
- Deleted item is no longer fetchable

### fetchAll()
- Returns all saved items
- Returns empty array when store is empty
- Items are in consistent order (insertion or specified)

### Invariants
- count == fetchAll().count (always)
- save then fetch returns same item
- delete then fetch returns nil
```

### Phase 3: Generate Contract Test Suite

#### Pattern: Generic Test Suite

```swift
import Testing

/// Contract tests for any DataStore implementation.
/// Subclass or call with your concrete implementation.
struct DataStoreContractTests<Store: DataStore> {

    let makeStore: () -> Store

    init(makeStore: @escaping () -> Store) {
        self.makeStore = makeStore
    }

    // MARK: - save(_ item:)

    @Test("save increases count")
    func saveIncreasesCount() async throws {
        let store = makeStore()
        let item = Item(id: "1", title: "Test")

        try await store.save(item)

        #expect(store.count == 1)
    }

    @Test("save then fetch returns same item")
    func saveAndFetch() async throws {
        let store = makeStore()
        let item = Item(id: "1", title: "Test")

        try await store.save(item)
        let fetched = try await store.fetch(id: "1")

        #expect(fetched?.title == "Test")
    }

    @Test("save existing item updates it")
    func saveUpdates() async throws {
        let store = makeStore()
        try await store.save(Item(id: "1", title: "Original"))
        try await store.save(Item(id: "1", title: "Updated"))

        let fetched = try await store.fetch(id: "1")

        #expect(fetched?.title == "Updated")
        #expect(store.count == 1)
    }

    @Test("save throws on invalid item")
    func saveInvalidThrows() async {
        let store = makeStore()
        let invalid = Item(id: "1", title: "")

        await #expect(throws: DataStoreError.invalidItem) {
            try await store.save(invalid)
        }
    }

    // MARK: - fetch(id:)

    @Test("fetch returns nil for non-existent ID")
    func fetchNonExistent() async throws {
        let store = makeStore()

        let result = try await store.fetch(id: "nonexistent")

        #expect(result == nil)
    }

    // MARK: - delete(id:)

    @Test("delete removes item")
    func deleteRemoves() async throws {
        let store = makeStore()
        try await store.save(Item(id: "1", title: "Test"))

        try await store.delete(id: "1")

        #expect(store.count == 0)
        let fetched = try await store.fetch(id: "1")
        #expect(fetched == nil)
    }

    @Test("delete non-existent does nothing")
    func deleteNonExistent() async throws {
        let store = makeStore()

        try await store.delete(id: "nonexistent")

        #expect(store.count == 0)
    }

    // MARK: - fetchAll()

    @Test("fetchAll returns all items")
    func fetchAll() async throws {
        let store = makeStore()
        try await store.save(Item(id: "1", title: "A"))
        try await store.save(Item(id: "2", title: "B"))

        let all = try await store.fetchAll()

        #expect(all.count == 2)
    }

    @Test("fetchAll returns empty when store is empty")
    func fetchAllEmpty() async throws {
        let store = makeStore()

        let all = try await store.fetchAll()

        #expect(all.isEmpty)
    }

    // MARK: - Invariants

    @Test("count matches fetchAll count")
    func countInvariant() async throws {
        let store = makeStore()
        try await store.save(Item(id: "1", title: "A"))
        try await store.save(Item(id: "2", title: "B"))
        try await store.delete(id: "1")

        let all = try await store.fetchAll()

        #expect(store.count == all.count)
    }
}
```

#### Running Contract Tests Per Implementation

```swift
@Suite("InMemoryDataStore Contract")
struct InMemoryDataStoreContractTests {
    let contract = DataStoreContractTests { InMemoryDataStore() }

    @Test("save increases count")
    func saveIncreasesCount() async throws {
        try await contract.saveIncreasesCount()
    }

    @Test("save then fetch returns same item")
    func saveAndFetch() async throws {
        try await contract.saveAndFetch()
    }

    // ... all contract tests
}

@Suite("SQLiteDataStore Contract")
struct SQLiteDataStoreContractTests {
    let contract = DataStoreContractTests { SQLiteDataStore(path: ":memory:") }

    @Test("save increases count")
    func saveIncreasesCount() async throws {
        try await contract.saveIncreasesCount()
    }

    // ... all contract tests
}
```

#### Alternative: Parameterized by Implementation

```swift
@Suite("DataStore Contract")
struct DataStoreContractSuite {

    enum StoreType: String, CaseIterable {
        case inMemory, sqlite, cloud
    }

    func makeStore(_ type: StoreType) -> any DataStore {
        switch type {
        case .inMemory: return InMemoryDataStore()
        case .sqlite: return SQLiteDataStore(path: ":memory:")
        case .cloud: return MockCloudDataStore()
        }
    }

    @Test("save increases count", arguments: StoreType.allCases)
    func saveIncreasesCount(type: StoreType) async throws {
        let store = makeStore(type)
        try await store.save(Item(id: "1", title: "Test"))
        #expect(store.count == 1)
    }

    @Test("fetch non-existent returns nil", arguments: StoreType.allCases)
    func fetchNonExistent(type: StoreType) async throws {
        let store = makeStore(type)
        let result = try await store.fetch(id: "nonexistent")
        #expect(result == nil)
    }
}
```

## Common Contract Categories

### Repository / Data Store
- CRUD operations (create, read, update, delete)
- Query/filter behavior
- Ordering guarantees
- Uniqueness constraints
- Empty state handling

### Network Client
- Successful response parsing
- Error response handling (4xx, 5xx)
- Timeout behavior
- Retry logic
- Request construction (headers, body)

### Cache
- Store and retrieve
- Expiration/TTL
- Eviction policy (LRU, size limit)
- Thread safety
- Persistence across init

### Authentication
- Login with valid credentials
- Reject invalid credentials
- Token refresh
- Session expiration
- Logout clears state

## Output Format

```markdown
## Contract Tests: [Protocol Name]

### Protocol
```swift
protocol DataStore { ... }
```

### Behaviors Tested
| Behavior | Tests | Edge Cases |
|----------|-------|------------|
| save | 3 | empty title, duplicate ID |
| fetch | 2 | non-existent ID |
| delete | 2 | non-existent ID |
| fetchAll | 2 | empty store |
| invariants | 1 | count consistency |

### Implementations Tested
- [x] InMemoryDataStore — all passing
- [x] SQLiteDataStore — all passing
- [ ] CloudDataStore — pending implementation

### Files Created
- `Tests/Contracts/DataStoreContractTests.swift`
- `Tests/Contracts/InMemoryDataStoreContractTests.swift`
```

## References

- `testing/tdd-feature/` — for TDD workflow using contract tests
- `generators/test-generator/` — for standard test generation
- Design by Contract (Bertrand Meyer) — theoretical foundation

Overview

This skill generates reusable contract test suites for Swift protocols so every implementation can be validated against the same behavioral specification. Define the protocol contract once and produce generic tests that concrete implementations (real, mock, or AI-generated) must pass. It helps prevent regressions when swapping data stores, network clients, caches, or auth backends.

How this skill works

The skill inspects a protocol signature and extracts required methods, properties, and invariants, then maps each element to clear behavioral assertions (success, failure modes, edge cases, and invariants). It emits a generic, parameterizable Swift test suite (async-safe) that can be instantiated per implementation or run parametrically across implementations. Tests include CRUD, error paths, ordering, thread-safety considerations, and explicit invariants like count consistency.

When to use it

  • Designing a new protocol and needing a single source of truth for behavior
  • You have multiple implementations (in-memory, DB, cloud, mock) and must ensure parity
  • Swapping persistence layers (e.g., CoreData → SwiftData) or client implementations
  • Validating AI-generated or third-party implementations against your spec
  • Adding regression protection when evolving protocol contracts

Best practices

  • Write clear, minimal protocol docs describing expected side effects and invariants before generating tests
  • Cover success paths, common error cases, and boundary conditions for each method
  • Keep contract tests implementation-agnostic: use factory closures to create instances
  • Include invariants as explicit tests (e.g., count == fetchAll().count) and run them after mutations
  • Run contract suites in CI for every implementation and fail builds on divergence

Example use cases

  • Generate a DataStore contract suite covering save/fetch/delete/fetchAll and invariants, then run it against InMemory, SQLite, and Cloud stores
  • Create a NetworkClient contract that asserts parsing, 4xx/5xx handling, timeout and retry behavior across mock and real clients
  • Build a Cache contract testing TTL, eviction policy, persistence across init, and thread-safety for LRU and memory caches
  • Validate an Authentication contract that checks login success, rejection of bad credentials, token refresh, and logout state clearing

FAQ

How do I apply the generated contract to a concrete implementation?

Provide a factory closure (makeStore / makeClient) that constructs the concrete type and call or subclass the generic contract tests for that factory; each test then runs against the instance you supply.

Can contract tests cover asynchronous and error scenarios?

Yes. Generated suites use async tests and explicit error expectations so you can assert thrown errors, timeouts, and race-sensitive invariants.

What if an implementation intentionally diverges from the contract?

A failing contract test signals behavioral mismatch. If divergence is intentional, update the protocol/spec and regenerate the contract, documenting the change so all implementations can be updated.