Replay: Modernizing Swift Network Testing with HTTP Traffic Recording

The year 2025 presents Swift developers with a familiar challenge: effectively testing network-dependent code within applications. The traditional approaches often fall short. Directly interacting with live APIs during testing, while seemingly straightforward, introduces significant drawbacks. These tests become prone to slowness, flakiness, and unpredictable failures, especially when external services experience outages or performance degradation. An alternative, stubbing URLSession, necessitates maintaining duplicate implementations of the networking layer, a cumbersome and error-prone process. Manually curating JSON fixture files offers another path, but it lacks automated capture of real responses, leading to fixtures that quickly become outdated and irrelevant without any indication.
Fortunately, a more robust and battle-tested methodology has emerged: record real HTTP traffic once, and then replay it instantaneously for every subsequent test run. This pattern, refined over more than fifteen years in various programming languages, is now accessible to Swift developers through a new tool called Replay.
The Genesis of HTTP Recording in Testing
The concept of capturing and replaying network interactions for testing gained significant traction in 2010 when Myron Marston introduced VCR for Ruby. Drawing an analogy to videocassette recorders that captured television broadcasts for later playback, VCR enabled tests to capture HTTP communications, thereby eliminating the need for live network calls during subsequent test executions. The mantra was simple and powerful: "Record once, play back forever."
This innovative approach quickly resonated within the developer community, inspiring similar implementations across a multitude of programming languages. Python saw the development of VCR.py and pytest-recording, further solidifying the pattern. The home video theme continued with Java’s Betamax, while Go adopted the methodology with go-vcr. These tools empowered developers to create more stable, reliable, and efficient testing suites.
For years, developers like the author of this piece, who had benefited from VCR and pytest-recording, yearned for a comparable solution in the Swift ecosystem. While Venmo’s DVR offered a partial solution by leveraging URLProtocol injection, it was designed for an earlier era of Swift development. The evolution of Swift’s tooling and language features has now paved the way for a more integrated and user-friendly experience.
The Modern Swift Approach: Replay and Its Innovations
Replay emerges as a significant advancement, built upon the lessons learned from its predecessors and leveraging contemporary Swift capabilities. Its core innovation lies in its adoption of the HTTP Archive (HAR) format and its seamless integration with Swift’s modern testing frameworks.
A key differentiator for Replay is its embrace of HAR as a standard for storing HTTP archives. In the early days of VCR, a custom YAML format was devised due to the absence of a widely adopted standard. However, HAR, developed by Jan Odvarko within the Firefox developer tools team, has since become the de facto industry standard. Today, HAR files are readily exportable from all major web browsers, as well as popular network debugging tools like Charles Proxy, Proxyman, mitmproxy, and Postman. This standardization allows Replay users to easily import traffic captured from browser developer tools directly into their test fixtures, and to inspect these fixtures using any standard text editor.
Furthermore, Replay capitalizes on the enhanced extension points now available in Swift. The introduction of Swift Testing traits, particularly the TestScoping protocol in Swift 6.1, enables declarative, per-test configuration that mirrors the capabilities of frameworks like pytest. Coupled with Swift Package Manager plugins, Replay offers integrated tooling that feels native to the Swift development environment, a significant improvement over previous attempts. These advancements simply did not exist in a way that was both intuitive and convenient for Swift developers until now.
Understanding Replay’s Mechanics: Recording and Playback
At its heart, Replay operates on a straightforward principle: when a test is annotated with the .replay trait, Replay intercepts all outgoing HTTP requests. Instead of reaching the live network, these requests are serviced by responses stored within a HAR file. This process significantly accelerates test execution and ensures deterministic results.
Consider a typical Swift test case for fetching user data:
import Testing
import Replay
struct User: Codable
let id: Int
let name: String
let email: String
@Test(.replay("fetchUser"))
func fetchUser() async throws
let (data, _) = try await URLSession.shared.data(
from: URL(string: "https://api.example.com/users/42")!
)
let user = try JSONDecoder().decode(User.self, from: data)
#expect(user.id == 42)
The .replay("fetchUser") trait instructs Replay to look for responses within a HAR file named fetchUser.har, located in a Replays directory. Crucially, the production code remains unchanged, continuing to use URLSession as it normally would. Replay achieves this interception through built-in affordances within the Foundation URL Loading System, ensuring compatibility with URLSession.shared, custom URLSession instances, and popular networking libraries like Alamofire.
The Deliberate Recording Workflow: Ensuring Security and Awareness
Replay employs a deliberate and secure workflow for recording network traffic. The first time a test marked with .replay is executed, it will intentionally fail, indicating that no corresponding entry exists in the archive. This behavior is a critical security feature. Accidental recording could inadvertently capture sensitive information such as credentials, personally identifiable information (PII), or session tokens. By requiring explicit opt-in for recording, Replay ensures that developers are always aware when network traffic is being captured, mitigating the risk of data leakage.
The failure message provides clear guidance on how to proceed:
○─ Test fetchUser() recorded an issue at ExampleTests.swift
ℹ︎─ No Matching Entry in Archive
Request: GET https://api.example.com/users/42
Archive: /path/to/.../Replays/fetchUser.har
This request was not found in the replay archive.
Options:
1. Run against the live network:
REPLAY_PLAYBACK_MODE=live swift test --filter <test-name>
2. Record the archive:
REPLAY_RECORD_MODE=once swift test --filter <test-name>
To initiate recording, developers are instructed to run the test with the REPLAY_RECORD_MODE=once environment variable:
REPLAY_RECORD_MODE=once swift test --filter fetchUser
This command triggers the test to execute against the live API. Upon receiving the response, Replay captures it and saves it into the Replays/fetchUser.har file. Subsequent executions of this test will then utilize this recorded fixture, leading to instantaneous results. The satisfaction of witnessing a formerly time-consuming test suite complete in mere seconds is a tangible benefit of this workflow.
Inside the HAR File: Structure and Versatility
A HAR file is fundamentally a JSON document that archives the details of HTTP requests and responses. Its structure is designed to be human-readable and machine-parseable, making it a versatile format. A typical HAR entry might look like this:
"log":
"version": "1.2",
"creator": "name": "Replay", "version": "1.0" ,
"entries": [
"request":
"method": "GET",
"url": "https://api.example.com/users/42",
"headers": ["name": "Accept", "value": "application/json"]
,
"response":
"status": 200,
"content":
"mimeType": "application/json",
"text": ""id":42,"name":"Alice""
]
This JSON structure provides a comprehensive record of the interaction, including the request method, URL, headers, and the response status code and body. The human-readable nature of HAR files, combined with their compatibility with a wide array of tools, makes them an exceptionally practical choice for test fixture management.
Safeguarding Sensitive Data: Filtering Mechanisms
The convenience of HAR files in capturing detailed network interactions also presents a challenge: the potential inclusion of sensitive data. Session cookies, authorization headers, and API keys can all be inadvertently recorded alongside the desired response content. Replay addresses this concern with a robust filtering mechanism that allows developers to scrub sensitive information during the recording process.
This is achieved by configuring filters directly within the test annotation:
@Test(
.replay(
"fetchUser",
filters: [
.headers(removing: ["Authorization", "Cookie"]),
.queryParameters(removing: ["token", "api_key"])
]
)
)
func fetchUser() async throws /* ... */
This configuration ensures that sensitive headers like "Authorization" and "Cookie," along with query parameters such as "token" and "api_key," are removed from the recorded HAR file. The guiding principle is to establish these filters before recording, not as a post-hoc cleanup operation. This proactive approach guarantees that sensitive data is never committed to the repository in the first place.
Flexible Matching Strategies for Diverse APIs
By default, Replay matches incoming HTTP requests against recorded responses based on the HTTP method and the complete URL. However, many real-world APIs utilize dynamic query parameters for purposes such as pagination cursors or cache-busting timestamps. To accommodate these scenarios, Replay offers flexible matching strategies.
Developers can configure looser matching criteria to ensure that tests continue to function even when such parameters vary:
@Test(.replay("fetchUser", matching: [.method, .path]))
func fetchUser() async throws /* ... */
In this example, the test will match requests based on the HTTP method and the URL path, ignoring any variations in query parameters. Replay provides a comprehensive set of matchers, including .method, .url, .host, .path, .query, .headers([...]), .body, and a .custom(...) option for defining arbitrary matching logic. This flexibility ensures that Replay can effectively handle a wide spectrum of API designs.
Inline Stubs for Granular Control
Beyond recording live traffic, Replay also supports inline stubbing, offering a convenient way to define mock responses directly within the test code. This is particularly useful for testing error handling, edge cases, or scenarios where the specific content of a response is less critical than its status code or headers.
@Test(
.replay(
stubs: [
.get("https://api.example.com/health", 200, ["Content-Type": "application/json"])
#""status": "ok""#
]
)
)
func testHealthCheck() async throws
let (data, _) = try await URLSession.shared.data(
from: URL(string: "https://api.example.com/health")!
)
#expect(String(data: data, encoding: .utf8) == #""status": "ok""#)
This inline stub defines a GET request to /health that will return a 200 status code, a Content-Type header, and a JSON body indicating "ok" status. This capability allows developers to precisely control the test environment for specific scenarios without needing to rely on external fixtures or live network calls.
The Enduring Value of HTTP Recording in Swift
The fifteen years of refinement in ecosystems like Ruby, Python, and JavaScript have established clear best practices for HTTP recording in testing. These include the imperative to capture real traffic, meticulously strip sensitive data, implement fast-fail mechanisms for missing fixtures, and provide informative error messages when issues arise. Replay effectively brings all of these established principles to the Swift programming language.
For Swift developers who have grappled with the instability of API tests or have deferred testing network code due to perceived overhead, Replay offers a compelling solution. By adopting this approach, development teams can significantly enhance the reliability, speed, and maintainability of their testing suites.
Replay is available as an open-source project on GitHub, inviting community contributions and feedback. Its emergence marks a significant step forward in modernizing Swift’s testing capabilities, empowering developers to build more robust and resilient applications.







