piss

entries

  1. Replay
    NSHipster
  2. Manim
    NSHipster
  3. @isolated(any)
    NSHipster
  4. Uncertain⟨T⟩
    NSHipster
  5. Model Context Protocol (MCP)
    NSHipster
  6. Ollama
    NSHipster
  7. op run
    NSHipster
  8. As We May Code
    NSHipster
  9. WWDC 2020
    NSHipster
  10. Language Server Protocol
    NSHipster

Replay

NSHipster

source

<p>The year is 2025. You’re writing tests for networking code in your Swift app.</p> <p>You could hit the live API, but then your tests are slow, flaky, and fail whenever that third-party service has a bad day. You could stub <code>URLSession</code>, but then you’re maintaining two implementations of your networking layer. You could maintain JSON fixtures by hand, but there’s no way to capture real responses automatically, so your fixtures go stale without anyone noticing.</p> <p>There’s a better approach: <strong>Record real HTTP traffic once, then replay it instantly for every subsequent test run.</strong> This pattern has been battle-tested for over fifteen years in other languages.</p> <p><a href="https://github.com/mattt/Replay">Replay</a> brings it to Swift.</p> <hr /><a id="get-on-with-it"></a> <p>In February 2010, <a href="https://github.com/myronmarston">Myron Marston</a> created <a href="https://github.com/vcr/vcr">VCR</a> for Ruby. Just like a videocassette recorder could capture television for later playback, VCR captured HTTP interactions so tests could replay them without hitting the network. Record once, play back forever.</p> <p>The idea spread. Python got <a href="https://github.com/kevin1024/vcrpy">VCR.py</a> and <a href="https://github.com/kiwicom/pytest-recording">pytest-recording</a>. Java got <a href="https://github.com/betamaxteam/betamax">Betamax</a> (continuing the home video theme). Go got <a href="https://github.com/dnaeon/go-vcr">go-vcr</a>.</p> <p>I’ve used VCR and pytest-recording for years and always missed having something comparable in Swift. <a href="https://github.com/venmo/DVR">DVR</a> from Venmo came closest, using the same <code>URLProtocol</code> injection point that Replay uses. But it was built for a different era of Swift — before we had the tools to make something that felt really nice.</p> <h3> <a class="anchor" href="https://nshipster.com/replay/#for-auld-lang-syne" id="for-auld-lang-syne"></a>For Auld Lang Syne</h3> <p>Being in conversation with that prior art means asking what’s different now. Two things stand out:</p> <p>First, <a href="https://en.wikipedia.org/wiki/HAR_%28file_format%29">HAR</a> became a <em>de facto</em> standard. When Myron built VCR, there was no widely-adopted format for HTTP archives, so he invented one using YAML. That’s where the “cassette” terminology comes from. But around the same time, Jan Odvarko on the Firefox developer tools team was creating HAR (HTTP Archive). Today, every major browser exports HAR. As do Charles Proxy, Proxyman, mitmproxy, and Postman.</p> <p>Replay uses HAR instead of inventing a new format. This means you can capture traffic from Safari’s Network tab and drop it directly into your test fixtures. You can inspect fixtures with any text editor.</p> <p>Second, Swift finally has the extension points we need. Swift Testing traits — especially the <a href="https://developer.apple.com/documentation/testing/testscoping"><code>TestScoping</code> protocol</a> from Swift 6.1 — enable the kind of declarative, per-test configuration that pytest fixtures provide. Package plugins let us build integrated tooling that feels native.</p> <p>These capabilities simply didn’t exist before — not in any way that felt intuitive or convenient.</p> <hr /> <h2> <a class="anchor" href="https://nshipster.com/replay/#how-replay-works" id="how-replay-works"></a>How Replay Works</h2> <p>Add <code>.replay</code> to a test and Replay intercepts HTTP requests, serving responses from a HAR file instead of hitting the network:</p> <pre class="highlight"><code><span class="kd">import</span> <span class="kt">Testing</span> <span class="kd">import</span> <span class="kt">Replay</span> <span class="kd">struct</span> <span class="kt">User</span><span class="p">:</span> <span class="kt">Codable</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">id</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">let</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span> <span class="k">let</span> <span class="nv">email</span><span class="p">:</span> <span class="kt">String</span> <span class="p">}</span> <span class="kd">@Test</span><span class="p">(</span><span class="o">.</span><span class="nf">replay</span><span class="p">(</span><span class="s">"fetchUser"</span><span class="p">))</span> <span class="kd">func</span> <span class="nf">fetchUser</span><span class="p">()</span> <span class="n">async</span> <span class="k">throws</span> <span class="p">{</span> <span class="k">let</span> <span class="p">(</span><span class="nv">data</span><span class="p">,</span> <span class="nv">_</span><span class="p">)</span> <span class="o">=</span> <span class="k">try</span> <span class="n">await</span> <span class="kt">URLSession</span><span class="o">.</span><span class="n">shared</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span> <span class="nv">from</span><span class="p">:</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">string</span><span class="p">:</span> <span class="s">"https://api.example.com/users/42"</span><span class="p">)</span><span class="o">!</span> <span class="p">)</span> <span class="k">let</span> <span class="nv">user</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">JSONDecoder</span><span class="p">()</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">User</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">from</span><span class="p">:</span> <span class="n">data</span><span class="p">)</span> <span class="cp">#expect(user.id == 42)</span> <span class="p">}</span> </code></pre> <p>The <code>.replay("fetchUser")</code> trait loads responses from <code>Replays/fetchUser.har</code>. Your production code uses <code>URLSession</code> normally. No protocols to define. No mocks to inject. Replay’s interception uses built-in affordances in the <a href="https://developer.apple.com/documentation/foundation/url-loading-system">Foundation URL Loading System</a>, so it works with <code>URLSession.shared</code>, custom sessions, and libraries like <a href="https://nshipster.com/alamofire">Alamofire</a>.</p> <h3> <a class="anchor" href="https://nshipster.com/replay/#the-recording-workflow" id="the-recording-workflow"></a>The Recording Workflow</h3> <p>The first time you run a test with <code>.replay</code>, it fails intentionally:</p> <pre class="highlight"><code>❌ 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 &lt;test-name&gt; 2. Record the archive: REPLAY_RECORD_MODE=once swift test --filter &lt;test-name&gt; </code></pre> <p>This is deliberate. Accidental recording could capture credentials, PII, or session tokens. By requiring you to opt into recording explicitly, Replay ensures you’re always aware when network traffic is being captured.</p> <p>So let’s do that now:</p> <pre class="highlight"><code><span class="nv">REPLAY_RECORD_MODE</span><span class="o">=</span>once swift <span class="nb">test</span> <span class="nt">--filter</span> fetchUser </code></pre> <p>This hits the real API, captures the response, and saves it to <code>Replays/fetchUser.har</code>. From then on, the test runs instantly against the recorded fixture. There’s something deeply satisfying about watching a test suite that used to take minutes finish in seconds.</p> <h3> <a class="anchor" href="https://nshipster.com/replay/#whats-in-a-har-file" id="whats-in-a-har-file"></a>What’s in a HAR File</h3> <p>A HAR file is just JSON:</p> <pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"log"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.2"</span><span class="p">,</span><span class="w"> </span><span class="nl">"creator"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Replay"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"entries"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"request"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GET"</span><span class="p">,</span><span class="w"> </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.example.com/users/42"</span><span class="p">,</span><span class="w"> </span><span class="nl">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Accept"</span><span class="p">,</span><span class="w"> </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"response"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="nl">"content"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"mimeType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{</span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2">:42,</span><span class="se">\"</span><span class="s2">name</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">Alice</span><span class="se">\"</span><span class="s2">}"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> <p>Human-readable. Editable. Compatible with a vast ecosystem of tools. 🧑‍🍳💋</p> <h2> <a class="anchor" href="https://nshipster.com/replay/#handling-sensitive-data" id="handling-sensitive-data"></a>Handling Sensitive Data</h2> <p>HAR files have a way of accumulating secrets. Session cookies, authorization headers, API keys — all captured faithfully alongside the responses you actually care about.</p> <p>Replay addresses this with filters that strip sensitive data during recording:</p> <pre class="highlight"><code><span class="kd">@Test</span><span class="p">(</span> <span class="o">.</span><span class="nf">replay</span><span class="p">(</span> <span class="s">"fetchUser"</span><span class="p">,</span> <span class="nv">filters</span><span class="p">:</span> <span class="p">[</span> <span class="o">.</span><span class="nf">headers</span><span class="p">(</span><span class="nv">removing</span><span class="p">:</span> <span class="p">[</span><span class="s">"Authorization"</span><span class="p">,</span> <span class="s">"Cookie"</span><span class="p">]),</span> <span class="o">.</span><span class="nf">queryParameters</span><span class="p">(</span><span class="nv">removing</span><span class="p">:</span> <span class="p">[</span><span class="s">"token"</span><span class="p">,</span> <span class="s">"api_key"</span><span class="p">])</span> <span class="p">]</span> <span class="p">)</span> <span class="p">)</span> <span class="kd">func</span> <span class="nf">fetchUser</span><span class="p">()</span> <span class="n">async</span> <span class="k">throws</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span> </code></pre> <p>The general principle: configure filters before recording, not after.</p> <aside class="admonition warning"> <p>Always review HAR fixtures before committing.</p> <p>In October 2023, <a href="https://www.nightfall.ai/blog/okta-data-breach-what-happened-impact-and-security-lessons-learned">Okta suffered a breach</a> after attackers obtained HAR files containing session tokens. In response, Cloudflare released a <a href="https://har-sanitizer.pages.dev">HAR sanitizer</a> and Chrome added <a href="https://developer.chrome.com/docs/devtools/network/reference">built-in sanitization</a> to DevTools.</p> </aside> <h2> <a class="anchor" href="https://nshipster.com/replay/#flexible-matching" id="flexible-matching"></a>Flexible Matching</h2> <p>By default, Replay matches requests by HTTP method and full URL. For APIs with volatile query parameters (pagination cursors, timestamps, cache-busters), you can configure looser matching:</p> <pre class="highlight"><code><span class="kd">@Test</span><span class="p">(</span><span class="o">.</span><span class="nf">replay</span><span class="p">(</span><span class="s">"fetchUser"</span><span class="p">,</span> <span class="nv">matching</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="n">method</span><span class="p">,</span> <span class="o">.</span><span class="n">path</span><span class="p">]))</span> <span class="kd">func</span> <span class="nf">fetchUser</span><span class="p">()</span> <span class="n">async</span> <span class="k">throws</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span> </code></pre> <p>Available matchers include <code>.method</code>, <code>.url</code>, <code>.host</code>, <code>.path</code>, <code>.query</code>, <code>.headers([...])</code>, <code>.body</code>, and <code>.custom(...)</code> for arbitrary logic.</p> <h2> <a class="anchor" href="https://nshipster.com/replay/#inline-stubs" id="inline-stubs"></a>Inline Stubs</h2> <p>Replay also supports inline stubs:</p> <pre class="highlight"><code><span class="kd">@Test</span><span class="p">(</span> <span class="o">.</span><span class="nf">replay</span><span class="p">(</span> <span class="nv">stubs</span><span class="p">:</span> <span class="p">[</span> <span class="o">.</span><span class="nf">get</span><span class="p">(</span><span class="s">"https://api.example.com/health"</span><span class="p">,</span> <span class="mi">200</span><span class="p">,</span> <span class="p">[</span><span class="s">"Content-Type"</span><span class="p">:</span> <span class="s">"application/json"</span><span class="p">],</span> <span class="p">{</span> <span class="cp">#"{"status": "ok"}"#</span> <span class="p">})</span> <span class="p">]</span> <span class="p">)</span> <span class="p">)</span> <span class="kd">func</span> <span class="nf">testHealthCheck</span><span class="p">()</span> <span class="n">async</span> <span class="k">throws</span> <span class="p">{</span> <span class="k">let</span> <span class="p">(</span><span class="nv">data</span><span class="p">,</span> <span class="nv">_</span><span class="p">)</span> <span class="o">=</span> <span class="k">try</span> <span class="n">await</span> <span class="kt">URLSession</span><span class="o">.</span><span class="n">shared</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span> <span class="nv">from</span><span class="p">:</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">string</span><span class="p">:</span> <span class="s">"https://api.example.com/health"</span><span class="p">)</span><span class="o">!</span> <span class="p">)</span> <span class="cp">#expect(String(data: data, encoding: .utf8) == #"{"status": "ok"}"#)</span> <span class="p">}</span> </code></pre> <p>This is useful for testing error handling, edge cases, or scenarios where the response content matters less than the status code.</p> <aside class="admonition info"> <p>Martin Fowler’s 2006 article <a href="https://martinfowler.com/articles/mocksArentStubs.html">“Mocks Aren’t Stubs”</a> introduces a taxonomy of <em>test doubles</em>: <em>dummies</em> (passed but never used), <em>fakes</em> (working but simplified implementations), <em>stubs</em> (canned responses), <em>spies</em> (stubs that record calls), and <em>mocks</em> (objects with built-in expectations).</p> <p>According to this ontology, Replay’s fixtures are <em>stubs</em> — they provide predetermined responses without verifying how they’re called. But unlike hand-rolled stubs that drift from reality, these are captured from real API traffic, so they stay honest.</p> </aside> <hr /> <p>Fifteen years of refinement across Ruby, Python, JavaScript, and other ecosystems have established clear best practices for HTTP recording: capture real traffic, strip sensitive data, fail fast when fixtures are missing, provide good error messages when things go wrong. Replay brings all of that to Swift.</p> <p>If you’ve been struggling with flaky API tests, or putting off testing your network code because the overhead felt too high, resolve to give Replay a try.</p> <aside class="parenthetical"> <p>And if you’re reading this on New Year’s Eve, consider it a gift — patterns that have proven themselves, carried forward into a new year. 🥂</p> </aside> <p><a href="https://github.com/mattt/Replay">Replay is open source on GitHub.</a> Let me know what you think!</p>