Skip to content

Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests.

License

Notifications You must be signed in to change notification settings

grdsdev/swift-vcr

Repository files navigation

Swift VCR

A Swift port of the popular VCR Ruby gem. Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests.

Features

  • Record & Replay: Automatically record HTTP interactions and replay them in subsequent test runs
  • URLSession Support: Works seamlessly with Foundation's URLSession
  • Multiple Record Modes: Control when and how interactions are recorded
  • Flexible Matching: Match requests by method, URI, headers, or body
  • JSON Storage: Human-readable cassette files in JSON format
  • Thread-Safe: Built with Swift concurrency in mind
  • Swift 6 Ready: Fully compatible with Swift 6 and modern concurrency

Installation

Add Swift VCR to your Package.swift:

dependencies: [
    .package(url: "https://github.com/grdsdev/swift-vcr.git", from: "0.1.0")
]

Quick Start

1. Configure VCR

import VCR

// In your test setup
VCR.configure(cassetteLibraryDirectory: "fixtures/cassettes")

2. Use a Cassette

func testAPIRequest() async throws {
    // Create a VCR-enabled URLSession
    let session = VCR.urlSession()

    try await VCR.shared.useCassette("my_api_test") {
        let url = URL(string: "https://api.example.com/data")!
        let (data, response) = try await session.data(from: url)

        // Your assertions here
        #expect(response.statusCode == 200)
    }
}

3. Run Your Tests

  • First run: VCR records the HTTP interaction to fixtures/cassettes/my_api_test.json
  • Subsequent runs: VCR replays the recorded interaction - no network calls!

Usage

Record Modes

Control when interactions are recorded:

// Record once, then replay (default)
try await VCR.shared.useCassette("test", recordMode: .once) {
    // Your code
}

// Record new interactions not in cassette
try await VCR.shared.useCassette("test", recordMode: .newEpisodes) {
    // Your code
}

// Always record (overwrite cassette)
try await VCR.shared.useCassette("test", recordMode: .all) {
    // Your code
}

// Never record (error if interaction not found)
try await VCR.shared.useCassette("test", recordMode: .none) {
    // Your code
}

Request Matching

Choose how requests are matched to recorded interactions:

// Match by HTTP method and URI (default)
try await VCR.shared.useCassette("test", matcher: .methodAndURI) {
    // Your code
}

// Match by method, URI, and request body
try await VCR.shared.useCassette("test", matcher: .methodURIAndBody) {
    // Your code
}

Manual Cassette Management

For more control, manually insert and eject cassettes:

// Insert a cassette
try VCR.shared.insertCassette("my_cassette", recordMode: .once)

// Create VCR-enabled session and make requests
let session = VCR.urlSession()
let (data, _) = try await session.data(from: url)

// Eject and save
try VCR.shared.ejectCassette()

Advanced Configuration

let config = VCRConfiguration(
    cassetteLibraryDirectory: URL(fileURLWithPath: "/path/to/cassettes"),
    defaultRecordMode: .newEpisodes,
    defaultMatcher: .methodAndURI
)
VCR.shared.configure(config)

Cassette File Format

Cassettes are stored as JSON files with a clean, readable format:

{
  "name" : "my_api_test",
  "record_mode" : "once",
  "matcher" : "method_uri",
  "interactions" : [
    {
      "request" : {
        "method" : "GET",
        "url" : "https://api.example.com/data",
        "headers" : {
          "Accept" : "application/json"
        }
      },
      "response" : {
        "statusCode" : 200,
        "headers" : {
          "Content-Type" : "application/json"
        },
        "body" : "eyJzdGF0dXMiOiJvayJ9"
      },
      "recordedAt" : "2025-10-23T09:00:00Z"
    }
  ]
}

Important: URLSession Configuration

Swift VCR uses a custom URLProtocol to intercept HTTP requests. You must use a VCR-enabled URLSession:

// ✅ Correct - use VCR.urlSession()
let session = VCR.urlSession()

// ❌ Won't work - URLSession.shared doesn't use custom protocols
let session = URLSession.shared

Examples

Testing an API Client

import Testing
import VCR

@Suite("API Client Tests")
struct APIClientTests {
    @Test func fetchUser() async throws {
        VCR.configure(cassetteLibraryDirectory: "Tests/Fixtures/Cassettes")

        let client = APIClient(session: VCR.urlSession())

        let user = try await VCR.shared.useCassette("fetch_user") {
            try await client.fetchUser(id: 123)
        }

        #expect(user.name == "John Doe")
    }
}

POST Requests with Body Matching

@Test func createUser() async throws {
    VCR.configure(cassetteLibraryDirectory: "Tests/Fixtures")

    try await VCR.shared.useCassette("create_user", matcher: .methodURIAndBody) {
        var request = URLRequest(url: URL(string: "https://api.example.com/users")!)
        request.httpMethod = "POST"
        request.httpBody = try JSONEncoder().encode(newUser)

        let (data, response) = try await VCR.urlSession().data(for: request)
        #expect((response as! HTTPURLResponse).statusCode == 201)
    }
}

Differences from Ruby VCR

Swift VCR focuses on core functionality for Swift/URLSession:

  • URLSession only: Unlike Ruby VCR which supports multiple HTTP libraries, Swift VCR focuses on URLSession
  • JSON cassettes: Uses JSON instead of YAML for better Swift compatibility
  • Async/await first: Built for modern Swift concurrency
  • Type-safe: Leverages Swift's type system for safer cassette handling

Requirements

  • Swift 6.0+
  • iOS 13+, macOS 10.15+, or Linux

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License - see LICENSE file for details

Acknowledgments

Inspired by the excellent VCR Ruby gem by Myron Marston and the VCR contributors.

About

Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages