{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-06-migrating-700-tests-to-swift-testing-a-real-world-experience/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "![](https://steipete.me/assets/img/2025/migrating-700-tests-to-swift-testing/hero.png)\n\n**TL;DR**: I let Claude Code convert 700+ tests to Swift Testing, watched it fail spectacularly, created an AI-generated playbook, then watched it succeed brilliantly. The difference? Better instructions.\n\nI’ve been migrating my test suites from XCTest to [Swift Testing](https://developer.apple.com/xcode/swift-testing/). Between [Vibe Meter](https://github.com/steipete/VibeMeter) and [Code Looper](https://github.com/steipete/CodeLooper), that’s over 700 tests across 118 files. Here’s what I learned from letting AI help with the conversion and then systematically improving the results using my [Swift Testing playbook](https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95).\n\n## The Initial Attempt\n\nStarted with the laziest possible prompt:\n\n> Hey Claude Code, convert all these tests to swift-testing.\\\n>  I’ll go make coffee.\n\nThe initial results were… technically correct. The tests compiled. They even passed. But looking at the code, it was clear this was just XCTest wearing a Swift Testing costume.\n\nWhen AI gives you lemons:\n\n- `XCTestCase` classes became [`@Suite`](https://developer.apple.com/documentation/testing/suite) structs\n- `XCTAssert` calls turned into [`#expect`](https://developer.apple.com/documentation/testing/expect\\(_:_:sourceLocation:\\)) statements\n- `func testFoo()` transformed to [`@Test`](https://developer.apple.com/documentation/testing/test) `func foo()`\n\nBut it missed the deeper opportunities that Swift Testing provides. The real work began after my coffee kicked in.\n\n> **Full disclosure**: I actually tried Swift Testing last week but completely messed it up because I linked to the wrong version without understanding it was already integrated into Xcode. Shoutout to [Stuart](https://x.com/throwspace/status/1929658866804953371) who nudged me to try again. Sometimes you need that external push to revisit something you wrote off too quickly.\n\n## Creating a Systematic Approach\n\nInstead of manually fixing 700 tests, I did what any reasonable developer would do: I procrastinated by watching WWDC videos. Both [*Meet Swift Testing*](https://developer.apple.com/videos/play/wwdc2024/10179/) and [*Go further with Swift Testing*](https://developer.apple.com/videos/play/wwdc2024/10195/) sessions were eye-opening.\n\nBut here’s where it gets complicated. The WWDC videos referenced outdated APIs that confused my AI agents. Plus, Claude couldn’t access Apple’s documentation because it’s all JavaScript-rendered. I spent hours trying different approaches until I discovered [Firecrawl](https://www.firecrawl.dev/referral?rid=9CG538BE) (affiliate link, I need moar credits!), which converted Apple’s entire Swift Testing documentation into a massive Markdown file. (This experience inspired me to build [llm.codes](https://steipete.me/posts/llm-codes-transform-developer-docs), where anyone can now generate up-to-date documentation from Apple’s official sources.)\n\nSo I ended up with [two documents](https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95):\n\n1. The complete Swift Testing API documentation from Apple (via [Firecrawl](https://www.firecrawl.dev/referral?rid=9CG538BE))\n2. An actionable playbook with examples from WWDC videos\n\nI fed both into [Google’s AI Studio](https://aistudio.google.com/), asked it to correct the outdated API references, and let Gemini compile everything into a comprehensive [Swift Testing resource](https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95) with both the API documentation and actionable migration patterns.\n\n**Why go through all this effort?** Swift Testing is new and underrepresented in open source compared to mature ecosystems like TypeScript/React where AI has absorbed countless examples. For emerging technologies, you need to explicitly provide the knowledge that doesn’t exist in the training data yet.\n\nThe key insight: AI needs concrete patterns and examples, not just documentation. This playbook became my guide for teaching Claude Code how to write *idiomatic* Swift Testing code, providing:\n\n- Migration patterns with before/after examples\n- Best practices for each Swift Testing feature\n- Common pitfalls and how to avoid them\n- Specific guidance on when to use each feature\n\n## Round Two: AI Redemption Arc\n\nWith the playbook in hand, I gave Claude Code new instructions:\n\n> Read `swift-testing-playbook.md` and improve & refactor the tests. Periodically stop, compile, fix any build issues, commit, ensure everything is green locally and on CI, then continue until perfection.\n\n**Plot twist**: The first time I tried this, Claude got creative and started implementing [new test patterns from the playbook](https://github.com/steipete/CodeLooper/pull/8/commits/d1bb7ba75dfefc821d24d5cfff6c785cd92f9079) instead of converting existing tests. I had to clarify:\n\n![Claude getting carried away with implementing test patterns instead of converting existing tests](https://steipete.me/assets/img/2025/migrating-700-tests-to-swift-testing/claude-being-dumb.png)\n\n> Read `swift-testing-playbook.md` and `swift-testing-api.md` and refactor the **existing** tests in this codebase to use Swift Testing patterns.\n\nLesson learned: Be very specific with AI instructions. “Improve tests using these patterns” can mean “create new tests” or “convert existing tests.” The difference matters when you have existing test coverage to preserve.\n\nThis iterative approach revealed several patterns worth sharing:\n\n### 1\\. Nested Suites Bring Order to Chaos\n\nThe biggest transformation came from consolidating scattered test files into organized hierarchies. Vibe Meter’s AuthenticationTokenManager tests showed the most dramatic improvement:\n\n```plaintext\n// Before: 4 separate test files\nVibeMeterTests/\n├── AuthenticationTokenManagerCoreTests.swift\n├── AuthenticationTokenManagerCookieTests.swift  \n├── AuthenticationTokenManagerEdgeCasesTests.swift\n└── AuthenticationTokenManagerTests.swift\n```\n\nTo this beauty:\n\n```swift\n@Suite(\"Authentication Token Manager Tests\", .tags(.authentication, .unit))\n@MainActor\nstruct AuthenticationTokenManagerTests {\n    // MARK: - Core Functionality\n    @Suite(\"Core Functionality\", .tags(.fast))\n    struct Core {\n        let mockManager: MockAuthenticationTokenManager\n        \n        init() {\n            mockManager = MockAuthenticationTokenManager()\n        }\n        \n        @Test(\"save token success\", .tags(.critical, .requiresKeychain))\n        func saveTokenSuccess() async throws {\n            // Core authentication tests...\n        }\n    }\n    \n    // MARK: - Cookie Management\n    @Suite(\"Cookie Management\")\n    struct CookieManagement {\n        @Test(\"cookie extraction from headers\")\n        func cookieExtractionFromHeaders() async throws {\n            // Cookie-related tests...\n        }\n    }\n    \n    // MARK: - Edge Cases\n    @Suite(\"Edge Cases\", .tags(.edgeCase, .fast))\n    struct EdgeCases {\n        @Test(\"handles concurrent token updates\")\n        func handlesConcurrentTokenUpdates() async throws {\n            // Edge case tests...\n        }\n    }\n}\n```\n\n75% fewer files, with logical organization that mirrors the actual functionality. Each nested suite can have its own setup, and the test navigator in Xcode finally makes sense.\n\n### 2\\. Parameterized Tests Eliminate Copy-Paste Syndrome\n\nRemember writing the same test five times with different values? Vibe Meter’s currency conversion tests showcase Swift Testing’s superior parameterization:\n\nView Currency Conversion Test Implementation\n\n```swift\n// Before: Individual test methods for each scenario\nfunc testConvert_SmallAmount() { /* test code */ }\nfunc testConvert_LargeAmount() { /* test code */ }\nfunc testConvert_NegativeAmount() { /* test code */ }\nfunc testConvert_ZeroAmount() { /* test code */ }\nfunc testConvert_PrecisionAmount() { /* test code */ }\n// ... and many more\n\n// After: Sophisticated parameterized testing\n@Suite(\"Currency Conversion Tests\", .tags(.currency, .unit))\nstruct CurrencyConversionTests {\n    struct ConversionTestCase: Sendable, CustomTestStringConvertible {\n        let amount: Double\n        let rate: Double\n        let expected: Double\n        let description: String\n        \n        var testDescription: String {\n            \"$\\(amount) × \\(rate) → $\\(expected) (\\(description))\"\n        }\n    }\n    \n    static let conversionTestCases: [ConversionTestCase] = [\n        ConversionTestCase(100.0, rate: 0.85, expected: 85.0, \"USD to EUR conversion\"),\n        ConversionTestCase(0.0, rate: 0.85, expected: 0.0, \"zero amount conversion\"),\n        ConversionTestCase(100.0, rate: 1.0, expected: 100.0, \"same currency conversion\"),\n        ConversionTestCase(-100.0, rate: 0.85, expected: -85.0, \"negative amount conversion\"),\n        ConversionTestCase(1_000_000.0, rate: 0.85, expected: 850_000.0, \"large amount conversion\"),\n        ConversionTestCase(0.01, rate: 0.85, expected: 0.0085, \"small amount conversion\"),\n        ConversionTestCase(999.99, rate: 1.2345, expected: 1234.488, \"precision conversion\"),\n    ]\n    \n    @Test(\"Currency conversion calculations\", .tags(.critical), \n          arguments: CurrencyConversionTests.conversionTestCases)\n    func conversionCalculations(testCase: ConversionTestCase) async {\n        let result = await MainActor.run {\n            CurrencyConversionHelper.convert(amount: testCase.amount, rate: testCase.rate)\n        }\n        \n        let tolerance = testCase.expected.magnitude < 1.0 ? 0.0001 : 0.01\n        #expect(result.isApproximatelyEqual(to: testCase.expected, tolerance: tolerance))\n    }\n    \n    @Test(\"Invalid rate handling\", .tags(.edgeCase), \n          arguments: [nil, 0.0, -1.0, .infinity, .nan] as [Double?])\n    func invalidRateHandling(invalidRate: Double?) async {\n        let result = await MainActor.run {\n            CurrencyConversionHelper.convert(amount: 100.0, rate: invalidRate)\n        }\n        #expect(result == 100.0) // Should return original amount for invalid rates\n    }\n}\n```\n\nThe beauty? [`CustomTestStringConvertible`](https://developer.apple.com/documentation/testing/customteststringconvertible) makes each test case self-documenting in the test navigator with descriptions like “$100.0 × 0.85 → $85.0 (USD to EUR conversion)” instead of generic object names. Test failures become instantly readable, and we can test edge cases (nil, infinity, NaN) in a single elegant test.\n\n### 3\\. Instance Isolation Simplifies State Management\n\nSwift Testing creates a fresh instance for each test, eliminating shared state issues:\n\n```swift\n@Suite struct DatabaseTests {\n    let db: TestDatabase\n    let tempDir: URL\n    \n    init() throws {\n        tempDir = FileManager.default.temporaryDirectory\n            .appendingPathComponent(UUID().uuidString)\n        try FileManager.default.createDirectory(at: tempDir)\n        db = TestDatabase(path: tempDir)\n    }\n    \n    deinit {\n        try? FileManager.default.removeItem(at: tempDir)\n    }\n}\n```\n\nNo more worrying about test order or cleanup between tests. Each test gets its own clean instance.\n\n### 4\\. Better Error Handling and Assertions\n\n[Swift Testing](https://developer.apple.com/documentation/testing/) provides two types of assertions that behave very differently:\n\n```swift\n// #expect: Soft assertion - continues test on failure\n#expect(user.email == \"test@example.com\")\n#expect(user.isActive == true)\n// ↑ Both assertions run even if first one fails\n\n// #require: Hard assertion - stops test immediately on failure\nlet config = try #require(loadConfiguration())\n// ↑ Test stops here if config is nil, preventing crashes below\nlet apiKey = try #require(config.apiKey)\n```\n\nError handling is also more expressive than XCTest:\n\n```swift\n// Validate specific error types\n#expect(throws: NetworkError.self) {\n    try await api.fetchWithoutAuth()\n}\n\n// Ensure no errors\n#expect(throws: Never.self) {\n    try parseValidJSON(data)\n}\n```\n\n### 5\\. Time Limits Prevent CI Hangs\n\nPerformance tests were scattered across multiple files with no protection against runaway execution:\n\nView Performance Test Implementation\n\n```swift\n// Before: Manual timing with XCTest\nfunc testLogging_Performance() {\n    let iterations = 10_000\n    let testMessage = \"Performance test message\"\n    \n    measure {\n        for i in 0..<iterations {\n            LoggingService.info(\"\\(testMessage) \\(i)\")\n        }\n    }\n}\n\nfunc testCurrencyConversionPerformance() {\n    self.measure {\n        for _ in 0..<1000 {\n            _ = CurrencyHelper.convert(100.0, rate: 0.85)\n        }\n    }\n}\n\n// After: Clean, declarative performance tests\n@Suite(\"Performance Benchmarks\", .tags(.performance))\nstruct PerformanceBenchmarks {\n    @Test(\"Bulk currency conversion performance\", .timeLimit(.minutes(1)))\n    @MainActor\n    func bulkCurrencyConversionPerformance() {\n        // Test configuration\n        let amounts = Array(stride(from: 0.01, through: 10_000.0, by: 0.01))\n        let exchangeRates = [\"EUR\": 0.92, \"GBP\": 0.82, \"JPY\": 110.0, \"AUD\": 1.35, \"CAD\": 1.25]\n        \n        // Performance test\n        let startTime = Date()\n        for amount in amounts {\n            for (_, rate) in exchangeRates {\n                _ = CurrencyConversionHelper.convert(amount: amount, rate: rate)\n            }\n        }\n        let duration = Date().timeIntervalSince(startTime)\n        \n        // Verify performance\n        print(\"Converted \\(amounts.count * exchangeRates.count) values in \\(duration)s\")\n        #expect(duration < 10.0) // Framework enforces the 1-minute limit\n    }\n    \n    @Test(\"Logging throughput\", .timeLimit(.seconds(30)))\n    func loggingThroughput() async {\n        let iterations = 100_000\n        \n        // No manual measurement needed - framework handles it\n        for i in 0..<iterations {\n            LoggingService.debug(\"Test message \\(i)\")\n        }\n        \n        // Test will fail if it exceeds 30 seconds\n    }\n}\n```\n\nThis prevents runaway tests from blocking CI pipelines. Something I’ve dealt with too many times in XCTest.\n\n## Beyond Basic Conversion: Real Improvements\n\nThe mechanical migration was just the beginning. Here’s where Swift Testing really shines:\n\n### Meaningful Error Testing with #expect(throws:)\n\nStop settling for “it didn’t crash” assertions:\n\n```swift\n// Before: Wishful thinking\ndo {\n    try await manager.startListener()\n} catch {\n    #expect(error != nil) // Well, yes... that's why we're in catch\n}\n\n// After: Actual validation\n#expect(throws: URLError.self) {\n    try await api.fetchWithoutAuth()\n}\n\n// Even better: Test the specific error properties\ndo {\n    try await api.fetchWithoutAuth()\n    Issue.record(\"Expected URLError to be thrown\")\n} catch let error as URLError {\n    #expect(error.code == .notConnectedToInternet)\n}\n```\n\n### Memory Leak Detection Built Right In\n\nSwift Testing’s instance isolation makes leak detection elegant:\n\n```swift\n@Test(\"ThreadSafeBox instances are properly deallocated\")\nfunc threadSafeBoxMemoryLeaks() async throws {\n    weak var weakBox: ThreadSafeBox<String>?\n    \n    do {\n        let box = ThreadSafeBox(\"test-value\")\n        weakBox = box\n        #expect(weakBox != nil, \"Box should be alive within scope\")\n    }\n    \n    await Task.yield() // Allow deallocation\n    #expect(weakBox == nil, \"Box should be deallocated after scope ends\")\n}\n```\n\n### Descriptive Test Names That Tell Stories\n\nNo more cryptic function names:\n\n```swift\n// Before: What does this even test?\n@Test(\"windowControllerManagement\")\n\n// After: Crystal clear intent\n@Test(\"Window controller management handles creation and lifecycle\")\n@Test(\"Position saving and restoration persists window states\")\n@Test(\"AppleScript support methods provide automation capabilities\")\n```\n\n### Advanced Confirmation Patterns\n\nSwift Testing’s [`confirmation()`](https://developer.apple.com/documentation/testing/confirmation\\(_:expectedcount:sourcelocation:_:\\)) handles complex async scenarios elegantly:\n\n```swift\n@Test(\"Multi-step debouncer lifecycle\")\nfunc multiStepDebouncerLifecycle() async throws {\n    try await confirmation(\"Complete lifecycle\", expectedCount: 3) { confirmation in\n        // Step 1: Initial call\n        debouncer.call { confirmation() }\n        try await Task.sleep(for: .milliseconds(70))\n        \n        // Step 2: Second call after first completes\n        debouncer.call { confirmation() }\n        try await Task.sleep(for: .milliseconds(70))\n        \n        // Step 3: Final verification\n        confirmation()\n    }\n}\n```\n\n### Taming Flaky Tests with withKnownIssue\n\nEvery codebase has those intermittent failures. Swift Testing provides a civilized way to handle them with [`withKnownIssue`](https://developer.apple.com/documentation/testing/withknownissue\\(_:isintermittent:sourcelocation:_:\\)):\n\n```swift\n@Test(\"External API availability check\")\nfunc externalAPITest() async throws {\n    await withKnownIssue(\"External API may be temporarily unavailable\", isIntermittent: true) {\n        let url = URL(string: \"https://api.exchangerate-api.com/v4/latest/USD\")!\n        let (_, response) = try await URLSession.shared.data(from: url)\n        let httpResponse = response as! HTTPURLResponse\n        #expect(httpResponse.statusCode == 200, \"Exchange rate API should be available\")\n    }\n}\n```\n\n## The Results\n\nLooking at the final pull requests ([Vibe Meter PR #28](https://github.com/steipete/VibeMeter/pull/28), [Code Looper PR #8](https://github.com/steipete/CodeLooper/pull/8)), the transformation is dramatic:\n\n**Combined Results:**\n\n- **46% fewer test files** (91 → 49 files) through intelligent consolidation\n- **+3,258 lines of enhanced test code** across both projects\n  - Vibe Meter: 61 → 23 files, +1,006 lines\n  - Code Looper: 30 → 26 files, +2,252 lines\n- **Zero test duplication** thanks to parameterized tests\n- **Hierarchical organization** that actually makes sense in Xcode’s navigator\n- **Bulletproof error handling** with specific exception types\n- **CI that doesn’t hang** with proper timeouts on every async test\n\n**Why more lines of code?** Swift Testing prioritizes maintainability over brevity. Here’s a typical transformation:\n\n```swift\n// Before: XCTest (3 lines)\nfunc testCurrencyConversion() {\n    XCTAssertEqual(convert(100, rate: 0.85), 85.0, accuracy: 0.01)\n}\n\n// After: Swift Testing (5 lines)\n@Test(\"Currency conversion with EUR rate\", .tags(.currency))\nfunc currencyConversionEUR() {\n    let result = convert(amount: 100.0, rate: 0.85)\n    #expect(result.isApproximatelyEqual(to: 85.0, tolerance: 0.01))\n}\n```\n\nThe increase comes from descriptive test names, explicit tagging, better parameter naming, and structured test case types for parameterized tests.\n\nThis represents quality improvement, not bloat. Moving from cryptic but compact tests to self-documenting, maintainable test suites.\n\n## Key Takeaways\n\n1. **AI needs guidance**: The initial blind conversion was just the starting point. The real value came from providing structured patterns through the playbook.\n\n2. **Iterative refinement works**: The approach of compile-test-commit-repeat caught issues early and made debugging easier.\n\n3. **Swift Testing encourages better patterns**: Features like parameterized tests and instance isolation naturally lead to cleaner test design.\n\n4. **Migration reveals test quality issues**: This wasn’t just a syntax conversion. It was an opportunity to improve test architecture.\n\n## Additional AI Resources for Swift Testing\n\nFor Cursor users, there’s the [@Docs](https://docs.cursor.com/context/@-symbols/@-docs) context which [includes](https://raw.githubusercontent.com/getcursor/crawler/main/docs.jsonl) the Apple Documentation as a choice.\n\nThere’s also the [Context 7 MCP](https://context7.com/swiftlang/swift-testing) that can be instructed to fetch Apple’s docs for testing, however this only includes code snippets and no explanation. (Enes mentioned that [this is on purpose](https://x.com/enesakar/status/1931296179502927935), though I got great results with explanatory text)\n\nThe combination of AI assistance and systematic refinement made this large-scale migration manageable. While the initial AI conversion provided a foundation, the real value came from applying Swift Testing’s features thoughtfully to create a more maintainable test suite.\n\n## Running Tests Faster with Filters\n\nOne more tip that significantly speeds up the development workflow: use `swift test --filter` to run specific tests instead of the entire suite. This is especially useful when you’re iterating on a particular feature:\n\n```bash\n# Run a single test suite by name\nswift test --filter \"FooTests\"\n\n# Run tests with specific tags\nswift test --filter-tag fast\nswift test --filter-tag currency\n\n# Combine multiple tags (runs tests with ALL specified tags)\nswift test --filter-tag unit --filter-tag fast\n\n# Skip tests with certain tags\nswift test --skip-tag slow\nswift test --skip-tag requiresNetwork\n```\n\nThe performance difference is dramatic when working on a large codebase. Instead of waiting for 700+ tests to run, you can get feedback in seconds by filtering to just the tests you’re working on. Combined with Swift Testing’s parallel execution, this makes the test-driven development cycle incredibly fast.\n\n* * *\n\n**P.S.** - If you’re still manually converting `XCTestExpectation`, stop. [Make AI do it](https://steipete.me/posts/2025/claude-code-is-my-computer). Just give it [better instructions](https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95) than I did.\n",
  "authors": [],
  "captured_at": "2026-09-27T19:29:15.293681927+00:00",
  "category": "programming",
  "content_html": "<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://steipete.me/assets/img/2025/migrating-700-tests-to-swift-testing/hero.png\" alt=\"\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><strong>TL;DR</strong>: I let Claude Code convert 700+ tests to Swift Testing, watched it fail spectacularly, created an AI-generated playbook, then watched it succeed brilliantly. The difference? Better instructions.</p>\n<p>I’ve been migrating my test suites from XCTest to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/xcode/swift-testing/\">Swift Testing</a>. Between <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter\">Vibe Meter</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/CodeLooper\">Code Looper</a>, that’s over 700 tests across 118 files. Here’s what I learned from letting AI help with the conversion and then systematically improving the results using my <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95\">Swift Testing playbook</a>.</p>\n<h2>The Initial Attempt</h2>\n<p>Started with the laziest possible prompt:</p>\n<blockquote>\n<p>Hey Claude Code, convert all these tests to swift-testing.<br />\nI’ll go make coffee.</p>\n</blockquote>\n<p>The initial results were… technically correct. The tests compiled. They even passed. But looking at the code, it was clear this was just XCTest wearing a Swift Testing costume.</p>\n<p>When AI gives you lemons:</p>\n<ul>\n<li><code>XCTestCase</code> classes became <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/testing/suite\"><code>@Suite</code></a> structs</li>\n<li><code>XCTAssert</code> calls turned into <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/testing/expect(_:_:sourceLocation:)\"><code>#expect</code></a> statements</li>\n<li><code>func testFoo()</code> transformed to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/testing/test\"><code>@Test</code></a> <code>func foo()</code></li>\n</ul>\n<p>But it missed the deeper opportunities that Swift Testing provides. The real work began after my coffee kicked in.</p>\n<blockquote>\n<p><strong>Full disclosure</strong>: I actually tried Swift Testing last week but completely messed it up because I linked to the wrong version without understanding it was already integrated into Xcode. Shoutout to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/throwspace/status/1929658866804953371\">Stuart</a> who nudged me to try again. Sometimes you need that external push to revisit something you wrote off too quickly.</p>\n</blockquote>\n<h2>Creating a Systematic Approach</h2>\n<p>Instead of manually fixing 700 tests, I did what any reasonable developer would do: I procrastinated by watching WWDC videos. Both <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/videos/play/wwdc2024/10179/\"><em>Meet Swift Testing</em></a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/videos/play/wwdc2024/10195/\"><em>Go further with Swift Testing</em></a> sessions were eye-opening.</p>\n<p>But here’s where it gets complicated. The WWDC videos referenced outdated APIs that confused my AI agents. Plus, Claude couldn’t access Apple’s documentation because it’s all JavaScript-rendered. I spent hours trying different approaches until I discovered <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.firecrawl.dev/referral?rid=9CG538BE\">Firecrawl</a> (affiliate link, I need moar credits!), which converted Apple’s entire Swift Testing documentation into a massive Markdown file. (This experience inspired me to build <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/llm-codes-transform-developer-docs\">llm.codes</a>, where anyone can now generate up-to-date documentation from Apple’s official sources.)</p>\n<p>So I ended up with <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95\">two documents</a>:</p>\n<ol>\n<li>The complete Swift Testing API documentation from Apple (via <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.firecrawl.dev/referral?rid=9CG538BE\">Firecrawl</a>)</li>\n<li>An actionable playbook with examples from WWDC videos</li>\n</ol>\n<p>I fed both into <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://aistudio.google.com/\">Google’s AI Studio</a>, asked it to correct the outdated API references, and let Gemini compile everything into a comprehensive <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95\">Swift Testing resource</a> with both the API documentation and actionable migration patterns.</p>\n<p><strong>Why go through all this effort?</strong> Swift Testing is new and underrepresented in open source compared to mature ecosystems like TypeScript/React where AI has absorbed countless examples. For emerging technologies, you need to explicitly provide the knowledge that doesn’t exist in the training data yet.</p>\n<p>The key insight: AI needs concrete patterns and examples, not just documentation. This playbook became my guide for teaching Claude Code how to write <em>idiomatic</em> Swift Testing code, providing:</p>\n<ul>\n<li>Migration patterns with before/after examples</li>\n<li>Best practices for each Swift Testing feature</li>\n<li>Common pitfalls and how to avoid them</li>\n<li>Specific guidance on when to use each feature</li>\n</ul>\n<h2>Round Two: AI Redemption Arc</h2>\n<p>With the playbook in hand, I gave Claude Code new instructions:</p>\n<blockquote>\n<p>Read <code>swift-testing-playbook.md</code> and improve &amp; refactor the tests. Periodically stop, compile, fix any build issues, commit, ensure everything is green locally and on CI, then continue until perfection.</p>\n</blockquote>\n<p><strong>Plot twist</strong>: The first time I tried this, Claude got creative and started implementing <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/CodeLooper/pull/8/commits/d1bb7ba75dfefc821d24d5cfff6c785cd92f9079\">new test patterns from the playbook</a> instead of converting existing tests. I had to clarify:</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://steipete.me/assets/img/2025/migrating-700-tests-to-swift-testing/claude-being-dumb.png\" alt=\"Claude getting carried away with implementing test patterns instead of converting existing tests\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<blockquote>\n<p>Read <code>swift-testing-playbook.md</code> and <code>swift-testing-api.md</code> and refactor the <strong>existing</strong> tests in this codebase to use Swift Testing patterns.</p>\n</blockquote>\n<p>Lesson learned: Be very specific with AI instructions. “Improve tests using these patterns” can mean “create new tests” or “convert existing tests.” The difference matters when you have existing test coverage to preserve.</p>\n<p>This iterative approach revealed several patterns worth sharing:</p>\n<h3>1. Nested Suites Bring Order to Chaos</h3>\n<p>The biggest transformation came from consolidating scattered test files into organized hierarchies. Vibe Meter’s AuthenticationTokenManager tests showed the most dramatic improvement:</p>\n<pre><code class=\"language-plaintext\"><span class=\"code-snippet\">// Before: 4 separate test files\nVibeMeterTests/\n├── AuthenticationTokenManagerCoreTests.swift\n├── AuthenticationTokenManagerCookieTests.swift  \n├── AuthenticationTokenManagerEdgeCasesTests.swift\n└── AuthenticationTokenManagerTests.swift\n</span></code></pre>\n<p>To this beauty:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">@Suite(&quot;Authentication Token Manager Tests&quot;, .tags(.authentication, .unit))\n@MainActor\nstruct AuthenticationTokenManagerTests {\n    // MARK: - Core Functionality\n    @Suite(&quot;Core Functionality&quot;, .tags(.fast))\n    struct Core {\n        let mockManager: MockAuthenticationTokenManager\n        \n        init() {\n            mockManager = MockAuthenticationTokenManager()\n        }\n        \n        @Test(&quot;save token success&quot;, .tags(.critical, .requiresKeychain))\n        func saveTokenSuccess() async throws {\n            // Core authentication tests...\n        }\n    }\n    \n    // MARK: - Cookie Management\n    @Suite(&quot;Cookie Management&quot;)\n    struct CookieManagement {\n        @Test(&quot;cookie extraction from headers&quot;)\n        func cookieExtractionFromHeaders() async throws {\n            // Cookie-related tests...\n        }\n    }\n    \n    // MARK: - Edge Cases\n    @Suite(&quot;Edge Cases&quot;, .tags(.edgeCase, .fast))\n    struct EdgeCases {\n        @Test(&quot;handles concurrent token updates&quot;)\n        func handlesConcurrentTokenUpdates() async throws {\n            // Edge case tests...\n        }\n    }\n}\n</span></code></pre>\n<p>75% fewer files, with logical organization that mirrors the actual functionality. Each nested suite can have its own setup, and the test navigator in Xcode finally makes sense.</p>\n<h3>2. Parameterized Tests Eliminate Copy-Paste Syndrome</h3>\n<p>Remember writing the same test five times with different values? Vibe Meter’s currency conversion tests showcase Swift Testing’s superior parameterization:</p>\n<p>View Currency Conversion Test Implementation</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// Before: Individual test methods for each scenario\nfunc testConvert_SmallAmount() { /* test code */ }\nfunc testConvert_LargeAmount() { /* test code */ }\nfunc testConvert_NegativeAmount() { /* test code */ }\nfunc testConvert_ZeroAmount() { /* test code */ }\nfunc testConvert_PrecisionAmount() { /* test code */ }\n// ... and many more\n\n// After: Sophisticated parameterized testing\n@Suite(&quot;Currency Conversion Tests&quot;, .tags(.currency, .unit))\nstruct CurrencyConversionTests {\n    struct ConversionTestCase: Sendable, CustomTestStringConvertible {\n        let amount: Double\n        let rate: Double\n        let expected: Double\n        let description: String\n        \n        var testDescription: String {\n            &quot;$\\(amount) × \\(rate) → $\\(expected) (\\(description))&quot;\n        }\n    }\n    \n    static let conversionTestCases: [ConversionTestCase] = [\n        ConversionTestCase(100.0, rate: 0.85, expected: 85.0, &quot;USD to EUR conversion&quot;),\n        ConversionTestCase(0.0, rate: 0.85, expected: 0.0, &quot;zero amount conversion&quot;),\n        ConversionTestCase(100.0, rate: 1.0, expected: 100.0, &quot;same currency conversion&quot;),\n        ConversionTestCase(-100.0, rate: 0.85, expected: -85.0, &quot;negative amount conversion&quot;),\n        ConversionTestCase(1_000_000.0, rate: 0.85, expected: 850_000.0, &quot;large amount conversion&quot;),\n        ConversionTestCase(0.01, rate: 0.85, expected: 0.0085, &quot;small amount conversion&quot;),\n        ConversionTestCase(999.99, rate: 1.2345, expected: 1234.488, &quot;precision conversion&quot;),\n    ]\n    \n    @Test(&quot;Currency conversion calculations&quot;, .tags(.critical), \n          arguments: CurrencyConversionTests.conversionTestCases)\n    func conversionCalculations(testCase: ConversionTestCase) async {\n        let result = await MainActor.run {\n            CurrencyConversionHelper.convert(amount: testCase.amount, rate: testCase.rate)\n        }\n        \n        let tolerance = testCase.expected.magnitude &lt; 1.0 ? 0.0001 : 0.01\n        #expect(result.isApproximatelyEqual(to: testCase.expected, tolerance: tolerance))\n    }\n    \n    @Test(&quot;Invalid rate handling&quot;, .tags(.edgeCase), \n          arguments: [nil, 0.0, -1.0, .infinity, .nan] as [Double?])\n    func invalidRateHandling(invalidRate: Double?) async {\n        let result = await MainActor.run {\n            CurrencyConversionHelper.convert(amount: 100.0, rate: invalidRate)\n        }\n        #expect(result == 100.0) // Should return original amount for invalid rates\n    }\n}\n</span></code></pre>\n<p>The beauty? <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/testing/customteststringconvertible\"><code>CustomTestStringConvertible</code></a> makes each test case self-documenting in the test navigator with descriptions like “$100.0 × 0.85 → $85.0 (USD to EUR conversion)” instead of generic object names. Test failures become instantly readable, and we can test edge cases (nil, infinity, NaN) in a single elegant test.</p>\n<h3>3. Instance Isolation Simplifies State Management</h3>\n<p>Swift Testing creates a fresh instance for each test, eliminating shared state issues:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">@Suite struct DatabaseTests {\n    let db: TestDatabase\n    let tempDir: URL\n    \n    init() throws {\n        tempDir = FileManager.default.temporaryDirectory\n            .appendingPathComponent(UUID().uuidString)\n        try FileManager.default.createDirectory(at: tempDir)\n        db = TestDatabase(path: tempDir)\n    }\n    \n    deinit {\n        try? FileManager.default.removeItem(at: tempDir)\n    }\n}\n</span></code></pre>\n<p>No more worrying about test order or cleanup between tests. Each test gets its own clean instance.</p>\n<h3>4. Better Error Handling and Assertions</h3>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/testing/\">Swift Testing</a> provides two types of assertions that behave very differently:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// #expect: Soft assertion - continues test on failure\n#expect(user.email == &quot;test@example.com&quot;)\n#expect(user.isActive == true)\n// ↑ Both assertions run even if first one fails\n\n// #require: Hard assertion - stops test immediately on failure\nlet config = try #require(loadConfiguration())\n// ↑ Test stops here if config is nil, preventing crashes below\nlet apiKey = try #require(config.apiKey)\n</span></code></pre>\n<p>Error handling is also more expressive than XCTest:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// Validate specific error types\n#expect(throws: NetworkError.self) {\n    try await api.fetchWithoutAuth()\n}\n\n// Ensure no errors\n#expect(throws: Never.self) {\n    try parseValidJSON(data)\n}\n</span></code></pre>\n<h3>5. Time Limits Prevent CI Hangs</h3>\n<p>Performance tests were scattered across multiple files with no protection against runaway execution:</p>\n<p>View Performance Test Implementation</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// Before: Manual timing with XCTest\nfunc testLogging_Performance() {\n    let iterations = 10_000\n    let testMessage = &quot;Performance test message&quot;\n    \n    measure {\n        for i in 0..&lt;iterations {\n            LoggingService.info(&quot;\\(testMessage) \\(i)&quot;)\n        }\n    }\n}\n\nfunc testCurrencyConversionPerformance() {\n    self.measure {\n        for _ in 0..&lt;1000 {\n            _ = CurrencyHelper.convert(100.0, rate: 0.85)\n        }\n    }\n}\n\n// After: Clean, declarative performance tests\n@Suite(&quot;Performance Benchmarks&quot;, .tags(.performance))\nstruct PerformanceBenchmarks {\n    @Test(&quot;Bulk currency conversion performance&quot;, .timeLimit(.minutes(1)))\n    @MainActor\n    func bulkCurrencyConversionPerformance() {\n        // Test configuration\n        let amounts = Array(stride(from: 0.01, through: 10_000.0, by: 0.01))\n        let exchangeRates = [&quot;EUR&quot;: 0.92, &quot;GBP&quot;: 0.82, &quot;JPY&quot;: 110.0, &quot;AUD&quot;: 1.35, &quot;CAD&quot;: 1.25]\n        \n        // Performance test\n        let startTime = Date()\n        for amount in amounts {\n            for (_, rate) in exchangeRates {\n                _ = CurrencyConversionHelper.convert(amount: amount, rate: rate)\n            }\n        }\n        let duration = Date().timeIntervalSince(startTime)\n        \n        // Verify performance\n        print(&quot;Converted \\(amounts.count * exchangeRates.count) values in \\(duration)s&quot;)\n        #expect(duration &lt; 10.0) // Framework enforces the 1-minute limit\n    }\n    \n    @Test(&quot;Logging throughput&quot;, .timeLimit(.seconds(30)))\n    func loggingThroughput() async {\n        let iterations = 100_000\n        \n        // No manual measurement needed - framework handles it\n        for i in 0..&lt;iterations {\n            LoggingService.debug(&quot;Test message \\(i)&quot;)\n        }\n        \n        // Test will fail if it exceeds 30 seconds\n    }\n}\n</span></code></pre>\n<p>This prevents runaway tests from blocking CI pipelines. Something I’ve dealt with too many times in XCTest.</p>\n<h2>Beyond Basic Conversion: Real Improvements</h2>\n<p>The mechanical migration was just the beginning. Here’s where Swift Testing really shines:</p>\n<h3>Meaningful Error Testing with #expect(throws:)</h3>\n<p>Stop settling for “it didn’t crash” assertions:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// Before: Wishful thinking\ndo {\n    try await manager.startListener()\n} catch {\n    #expect(error != nil) // Well, yes... that&#39;s why we&#39;re in catch\n}\n\n// After: Actual validation\n#expect(throws: URLError.self) {\n    try await api.fetchWithoutAuth()\n}\n\n// Even better: Test the specific error properties\ndo {\n    try await api.fetchWithoutAuth()\n    Issue.record(&quot;Expected URLError to be thrown&quot;)\n} catch let error as URLError {\n    #expect(error.code == .notConnectedToInternet)\n}\n</span></code></pre>\n<h3>Memory Leak Detection Built Right In</h3>\n<p>Swift Testing’s instance isolation makes leak detection elegant:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">@Test(&quot;ThreadSafeBox instances are properly deallocated&quot;)\nfunc threadSafeBoxMemoryLeaks() async throws {\n    weak var weakBox: ThreadSafeBox&lt;String&gt;?\n    \n    do {\n        let box = ThreadSafeBox(&quot;test-value&quot;)\n        weakBox = box\n        #expect(weakBox != nil, &quot;Box should be alive within scope&quot;)\n    }\n    \n    await Task.yield() // Allow deallocation\n    #expect(weakBox == nil, &quot;Box should be deallocated after scope ends&quot;)\n}\n</span></code></pre>\n<h3>Descriptive Test Names That Tell Stories</h3>\n<p>No more cryptic function names:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// Before: What does this even test?\n@Test(&quot;windowControllerManagement&quot;)\n\n// After: Crystal clear intent\n@Test(&quot;Window controller management handles creation and lifecycle&quot;)\n@Test(&quot;Position saving and restoration persists window states&quot;)\n@Test(&quot;AppleScript support methods provide automation capabilities&quot;)\n</span></code></pre>\n<h3>Advanced Confirmation Patterns</h3>\n<p>Swift Testing’s <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/testing/confirmation(_:expectedcount:sourcelocation:_:)\"><code>confirmation()</code></a> handles complex async scenarios elegantly:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">@Test(&quot;Multi-step debouncer lifecycle&quot;)\nfunc multiStepDebouncerLifecycle() async throws {\n    try await confirmation(&quot;Complete lifecycle&quot;, expectedCount: 3) { confirmation in\n        // Step 1: Initial call\n        debouncer.call { confirmation() }\n        try await Task.sleep(for: .milliseconds(70))\n        \n        // Step 2: Second call after first completes\n        debouncer.call { confirmation() }\n        try await Task.sleep(for: .milliseconds(70))\n        \n        // Step 3: Final verification\n        confirmation()\n    }\n}\n</span></code></pre>\n<h3>Taming Flaky Tests with withKnownIssue</h3>\n<p>Every codebase has those intermittent failures. Swift Testing provides a civilized way to handle them with <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/testing/withknownissue(_:isintermittent:sourcelocation:_:)\"><code>withKnownIssue</code></a>:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">@Test(&quot;External API availability check&quot;)\nfunc externalAPITest() async throws {\n    await withKnownIssue(&quot;External API may be temporarily unavailable&quot;, isIntermittent: true) {\n        let url = URL(string: &quot;https://api.exchangerate-api.com/v4/latest/USD&quot;)!\n        let (_, response) = try await URLSession.shared.data(from: url)\n        let httpResponse = response as! HTTPURLResponse\n        #expect(httpResponse.statusCode == 200, &quot;Exchange rate API should be available&quot;)\n    }\n}\n</span></code></pre>\n<h2>The Results</h2>\n<p>Looking at the final pull requests (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/pull/28\">Vibe Meter PR #28</a>, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/CodeLooper/pull/8\">Code Looper PR #8</a>), the transformation is dramatic:</p>\n<p><strong>Combined Results:</strong></p>\n<ul>\n<li><strong>46% fewer test files</strong> (91 → 49 files) through intelligent consolidation</li>\n<li><strong>+3,258 lines of enhanced test code</strong> across both projects\n<ul>\n<li>Vibe Meter: 61 → 23 files, +1,006 lines</li>\n<li>Code Looper: 30 → 26 files, +2,252 lines</li>\n</ul>\n</li>\n<li><strong>Zero test duplication</strong> thanks to parameterized tests</li>\n<li><strong>Hierarchical organization</strong> that actually makes sense in Xcode’s navigator</li>\n<li><strong>Bulletproof error handling</strong> with specific exception types</li>\n<li><strong>CI that doesn’t hang</strong> with proper timeouts on every async test</li>\n</ul>\n<p><strong>Why more lines of code?</strong> Swift Testing prioritizes maintainability over brevity. Here’s a typical transformation:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// Before: XCTest (3 lines)\nfunc testCurrencyConversion() {\n    XCTAssertEqual(convert(100, rate: 0.85), 85.0, accuracy: 0.01)\n}\n\n// After: Swift Testing (5 lines)\n@Test(&quot;Currency conversion with EUR rate&quot;, .tags(.currency))\nfunc currencyConversionEUR() {\n    let result = convert(amount: 100.0, rate: 0.85)\n    #expect(result.isApproximatelyEqual(to: 85.0, tolerance: 0.01))\n}\n</span></code></pre>\n<p>The increase comes from descriptive test names, explicit tagging, better parameter naming, and structured test case types for parameterized tests.</p>\n<p>This represents quality improvement, not bloat. Moving from cryptic but compact tests to self-documenting, maintainable test suites.</p>\n<h2>Key Takeaways</h2>\n<ol>\n<li>\n<p><strong>AI needs guidance</strong>: The initial blind conversion was just the starting point. The real value came from providing structured patterns through the playbook.</p>\n</li>\n<li>\n<p><strong>Iterative refinement works</strong>: The approach of compile-test-commit-repeat caught issues early and made debugging easier.</p>\n</li>\n<li>\n<p><strong>Swift Testing encourages better patterns</strong>: Features like parameterized tests and instance isolation naturally lead to cleaner test design.</p>\n</li>\n<li>\n<p><strong>Migration reveals test quality issues</strong>: This wasn’t just a syntax conversion. It was an opportunity to improve test architecture.</p>\n</li>\n</ol>\n<h2>Additional AI Resources for Swift Testing</h2>\n<p>For Cursor users, there’s the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.cursor.com/context/@-symbols/@-docs\">@Docs</a> context which <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://raw.githubusercontent.com/getcursor/crawler/main/docs.jsonl\">includes</a> the Apple Documentation as a choice.</p>\n<p>There’s also the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://context7.com/swiftlang/swift-testing\">Context 7 MCP</a> that can be instructed to fetch Apple’s docs for testing, however this only includes code snippets and no explanation. (Enes mentioned that <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/enesakar/status/1931296179502927935\">this is on purpose</a>, though I got great results with explanatory text)</p>\n<p>The combination of AI assistance and systematic refinement made this large-scale migration manageable. While the initial AI conversion provided a foundation, the real value came from applying Swift Testing’s features thoughtfully to create a more maintainable test suite.</p>\n<h2>Running Tests Faster with Filters</h2>\n<p>One more tip that significantly speeds up the development workflow: use <code>swift test --filter</code> to run specific tests instead of the entire suite. This is especially useful when you’re iterating on a particular feature:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Run a single test suite by name</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">swift</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> test<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>filter</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>FooTests<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Run tests with specific tags</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">swift</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> test<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>filter-tag</span> fast</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">swift</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> test<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>filter-tag</span> currency</span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Combine multiple tags (runs tests with ALL specified tags)</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">swift</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> test<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>filter-tag</span> unit<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>filter-tag</span> fast</span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Skip tests with certain tags</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">swift</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> test<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>skip-tag</span> slow</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">swift</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> test<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>skip-tag</span> requiresNetwork</span>\n</span></span></code></pre>\n<p>The performance difference is dramatic when working on a large codebase. Instead of waiting for 700+ tests to run, you can get feedback in seconds by filtering to just the tests you’re working on. Combined with Swift Testing’s parallel execution, this makes the test-driven development cycle incredibly fast.</p>\n<hr />\n<p><strong>P.S.</strong> - If you’re still manually converting <code>XCTestExpectation</code>, stop. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/2025/claude-code-is-my-computer\">Make AI do it</a>. Just give it <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95\">better instructions</a> than I did.</p>\n",
  "content_markdown": "![](https://steipete.me/assets/img/2025/migrating-700-tests-to-swift-testing/hero.png)\n\n**TL;DR**: I let Claude Code convert 700+ tests to Swift Testing, watched it fail spectacularly, created an AI-generated playbook, then watched it succeed brilliantly. The difference? Better instructions.\n\nI’ve been migrating my test suites from XCTest to [Swift Testing](https://developer.apple.com/xcode/swift-testing/). Between [Vibe Meter](https://github.com/steipete/VibeMeter) and [Code Looper](https://github.com/steipete/CodeLooper), that’s over 700 tests across 118 files. Here’s what I learned from letting AI help with the conversion and then systematically improving the results using my [Swift Testing playbook](https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95).\n\n## The Initial Attempt\n\nStarted with the laziest possible prompt:\n\n> Hey Claude Code, convert all these tests to swift-testing.\\\n>  I’ll go make coffee.\n\nThe initial results were… technically correct. The tests compiled. They even passed. But looking at the code, it was clear this was just XCTest wearing a Swift Testing costume.\n\nWhen AI gives you lemons:\n\n- `XCTestCase` classes became [`@Suite`](https://developer.apple.com/documentation/testing/suite) structs\n- `XCTAssert` calls turned into [`#expect`](https://developer.apple.com/documentation/testing/expect\\(_:_:sourceLocation:\\)) statements\n- `func testFoo()` transformed to [`@Test`](https://developer.apple.com/documentation/testing/test) `func foo()`\n\nBut it missed the deeper opportunities that Swift Testing provides. The real work began after my coffee kicked in.\n\n> **Full disclosure**: I actually tried Swift Testing last week but completely messed it up because I linked to the wrong version without understanding it was already integrated into Xcode. Shoutout to [Stuart](https://x.com/throwspace/status/1929658866804953371) who nudged me to try again. Sometimes you need that external push to revisit something you wrote off too quickly.\n\n## Creating a Systematic Approach\n\nInstead of manually fixing 700 tests, I did what any reasonable developer would do: I procrastinated by watching WWDC videos. Both [*Meet Swift Testing*](https://developer.apple.com/videos/play/wwdc2024/10179/) and [*Go further with Swift Testing*](https://developer.apple.com/videos/play/wwdc2024/10195/) sessions were eye-opening.\n\nBut here’s where it gets complicated. The WWDC videos referenced outdated APIs that confused my AI agents. Plus, Claude couldn’t access Apple’s documentation because it’s all JavaScript-rendered. I spent hours trying different approaches until I discovered [Firecrawl](https://www.firecrawl.dev/referral?rid=9CG538BE) (affiliate link, I need moar credits!), which converted Apple’s entire Swift Testing documentation into a massive Markdown file. (This experience inspired me to build [llm.codes](https://steipete.me/posts/llm-codes-transform-developer-docs), where anyone can now generate up-to-date documentation from Apple’s official sources.)\n\nSo I ended up with [two documents](https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95):\n\n1. The complete Swift Testing API documentation from Apple (via [Firecrawl](https://www.firecrawl.dev/referral?rid=9CG538BE))\n2. An actionable playbook with examples from WWDC videos\n\nI fed both into [Google’s AI Studio](https://aistudio.google.com/), asked it to correct the outdated API references, and let Gemini compile everything into a comprehensive [Swift Testing resource](https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95) with both the API documentation and actionable migration patterns.\n\n**Why go through all this effort?** Swift Testing is new and underrepresented in open source compared to mature ecosystems like TypeScript/React where AI has absorbed countless examples. For emerging technologies, you need to explicitly provide the knowledge that doesn’t exist in the training data yet.\n\nThe key insight: AI needs concrete patterns and examples, not just documentation. This playbook became my guide for teaching Claude Code how to write *idiomatic* Swift Testing code, providing:\n\n- Migration patterns with before/after examples\n- Best practices for each Swift Testing feature\n- Common pitfalls and how to avoid them\n- Specific guidance on when to use each feature\n\n## Round Two: AI Redemption Arc\n\nWith the playbook in hand, I gave Claude Code new instructions:\n\n> Read `swift-testing-playbook.md` and improve & refactor the tests. Periodically stop, compile, fix any build issues, commit, ensure everything is green locally and on CI, then continue until perfection.\n\n**Plot twist**: The first time I tried this, Claude got creative and started implementing [new test patterns from the playbook](https://github.com/steipete/CodeLooper/pull/8/commits/d1bb7ba75dfefc821d24d5cfff6c785cd92f9079) instead of converting existing tests. I had to clarify:\n\n![Claude getting carried away with implementing test patterns instead of converting existing tests](https://steipete.me/assets/img/2025/migrating-700-tests-to-swift-testing/claude-being-dumb.png)\n\n> Read `swift-testing-playbook.md` and `swift-testing-api.md` and refactor the **existing** tests in this codebase to use Swift Testing patterns.\n\nLesson learned: Be very specific with AI instructions. “Improve tests using these patterns” can mean “create new tests” or “convert existing tests.” The difference matters when you have existing test coverage to preserve.\n\nThis iterative approach revealed several patterns worth sharing:\n\n### 1\\. Nested Suites Bring Order to Chaos\n\nThe biggest transformation came from consolidating scattered test files into organized hierarchies. Vibe Meter’s AuthenticationTokenManager tests showed the most dramatic improvement:\n\n```plaintext\n// Before: 4 separate test files\nVibeMeterTests/\n├── AuthenticationTokenManagerCoreTests.swift\n├── AuthenticationTokenManagerCookieTests.swift  \n├── AuthenticationTokenManagerEdgeCasesTests.swift\n└── AuthenticationTokenManagerTests.swift\n```\n\nTo this beauty:\n\n```swift\n@Suite(\"Authentication Token Manager Tests\", .tags(.authentication, .unit))\n@MainActor\nstruct AuthenticationTokenManagerTests {\n    // MARK: - Core Functionality\n    @Suite(\"Core Functionality\", .tags(.fast))\n    struct Core {\n        let mockManager: MockAuthenticationTokenManager\n        \n        init() {\n            mockManager = MockAuthenticationTokenManager()\n        }\n        \n        @Test(\"save token success\", .tags(.critical, .requiresKeychain))\n        func saveTokenSuccess() async throws {\n            // Core authentication tests...\n        }\n    }\n    \n    // MARK: - Cookie Management\n    @Suite(\"Cookie Management\")\n    struct CookieManagement {\n        @Test(\"cookie extraction from headers\")\n        func cookieExtractionFromHeaders() async throws {\n            // Cookie-related tests...\n        }\n    }\n    \n    // MARK: - Edge Cases\n    @Suite(\"Edge Cases\", .tags(.edgeCase, .fast))\n    struct EdgeCases {\n        @Test(\"handles concurrent token updates\")\n        func handlesConcurrentTokenUpdates() async throws {\n            // Edge case tests...\n        }\n    }\n}\n```\n\n75% fewer files, with logical organization that mirrors the actual functionality. Each nested suite can have its own setup, and the test navigator in Xcode finally makes sense.\n\n### 2\\. Parameterized Tests Eliminate Copy-Paste Syndrome\n\nRemember writing the same test five times with different values? Vibe Meter’s currency conversion tests showcase Swift Testing’s superior parameterization:\n\nView Currency Conversion Test Implementation\n\n```swift\n// Before: Individual test methods for each scenario\nfunc testConvert_SmallAmount() { /* test code */ }\nfunc testConvert_LargeAmount() { /* test code */ }\nfunc testConvert_NegativeAmount() { /* test code */ }\nfunc testConvert_ZeroAmount() { /* test code */ }\nfunc testConvert_PrecisionAmount() { /* test code */ }\n// ... and many more\n\n// After: Sophisticated parameterized testing\n@Suite(\"Currency Conversion Tests\", .tags(.currency, .unit))\nstruct CurrencyConversionTests {\n    struct ConversionTestCase: Sendable, CustomTestStringConvertible {\n        let amount: Double\n        let rate: Double\n        let expected: Double\n        let description: String\n        \n        var testDescription: String {\n            \"$\\(amount) × \\(rate) → $\\(expected) (\\(description))\"\n        }\n    }\n    \n    static let conversionTestCases: [ConversionTestCase] = [\n        ConversionTestCase(100.0, rate: 0.85, expected: 85.0, \"USD to EUR conversion\"),\n        ConversionTestCase(0.0, rate: 0.85, expected: 0.0, \"zero amount conversion\"),\n        ConversionTestCase(100.0, rate: 1.0, expected: 100.0, \"same currency conversion\"),\n        ConversionTestCase(-100.0, rate: 0.85, expected: -85.0, \"negative amount conversion\"),\n        ConversionTestCase(1_000_000.0, rate: 0.85, expected: 850_000.0, \"large amount conversion\"),\n        ConversionTestCase(0.01, rate: 0.85, expected: 0.0085, \"small amount conversion\"),\n        ConversionTestCase(999.99, rate: 1.2345, expected: 1234.488, \"precision conversion\"),\n    ]\n    \n    @Test(\"Currency conversion calculations\", .tags(.critical), \n          arguments: CurrencyConversionTests.conversionTestCases)\n    func conversionCalculations(testCase: ConversionTestCase) async {\n        let result = await MainActor.run {\n            CurrencyConversionHelper.convert(amount: testCase.amount, rate: testCase.rate)\n        }\n        \n        let tolerance = testCase.expected.magnitude < 1.0 ? 0.0001 : 0.01\n        #expect(result.isApproximatelyEqual(to: testCase.expected, tolerance: tolerance))\n    }\n    \n    @Test(\"Invalid rate handling\", .tags(.edgeCase), \n          arguments: [nil, 0.0, -1.0, .infinity, .nan] as [Double?])\n    func invalidRateHandling(invalidRate: Double?) async {\n        let result = await MainActor.run {\n            CurrencyConversionHelper.convert(amount: 100.0, rate: invalidRate)\n        }\n        #expect(result == 100.0) // Should return original amount for invalid rates\n    }\n}\n```\n\nThe beauty? [`CustomTestStringConvertible`](https://developer.apple.com/documentation/testing/customteststringconvertible) makes each test case self-documenting in the test navigator with descriptions like “$100.0 × 0.85 → $85.0 (USD to EUR conversion)” instead of generic object names. Test failures become instantly readable, and we can test edge cases (nil, infinity, NaN) in a single elegant test.\n\n### 3\\. Instance Isolation Simplifies State Management\n\nSwift Testing creates a fresh instance for each test, eliminating shared state issues:\n\n```swift\n@Suite struct DatabaseTests {\n    let db: TestDatabase\n    let tempDir: URL\n    \n    init() throws {\n        tempDir = FileManager.default.temporaryDirectory\n            .appendingPathComponent(UUID().uuidString)\n        try FileManager.default.createDirectory(at: tempDir)\n        db = TestDatabase(path: tempDir)\n    }\n    \n    deinit {\n        try? FileManager.default.removeItem(at: tempDir)\n    }\n}\n```\n\nNo more worrying about test order or cleanup between tests. Each test gets its own clean instance.\n\n### 4\\. Better Error Handling and Assertions\n\n[Swift Testing](https://developer.apple.com/documentation/testing/) provides two types of assertions that behave very differently:\n\n```swift\n// #expect: Soft assertion - continues test on failure\n#expect(user.email == \"test@example.com\")\n#expect(user.isActive == true)\n// ↑ Both assertions run even if first one fails\n\n// #require: Hard assertion - stops test immediately on failure\nlet config = try #require(loadConfiguration())\n// ↑ Test stops here if config is nil, preventing crashes below\nlet apiKey = try #require(config.apiKey)\n```\n\nError handling is also more expressive than XCTest:\n\n```swift\n// Validate specific error types\n#expect(throws: NetworkError.self) {\n    try await api.fetchWithoutAuth()\n}\n\n// Ensure no errors\n#expect(throws: Never.self) {\n    try parseValidJSON(data)\n}\n```\n\n### 5\\. Time Limits Prevent CI Hangs\n\nPerformance tests were scattered across multiple files with no protection against runaway execution:\n\nView Performance Test Implementation\n\n```swift\n// Before: Manual timing with XCTest\nfunc testLogging_Performance() {\n    let iterations = 10_000\n    let testMessage = \"Performance test message\"\n    \n    measure {\n        for i in 0..<iterations {\n            LoggingService.info(\"\\(testMessage) \\(i)\")\n        }\n    }\n}\n\nfunc testCurrencyConversionPerformance() {\n    self.measure {\n        for _ in 0..<1000 {\n            _ = CurrencyHelper.convert(100.0, rate: 0.85)\n        }\n    }\n}\n\n// After: Clean, declarative performance tests\n@Suite(\"Performance Benchmarks\", .tags(.performance))\nstruct PerformanceBenchmarks {\n    @Test(\"Bulk currency conversion performance\", .timeLimit(.minutes(1)))\n    @MainActor\n    func bulkCurrencyConversionPerformance() {\n        // Test configuration\n        let amounts = Array(stride(from: 0.01, through: 10_000.0, by: 0.01))\n        let exchangeRates = [\"EUR\": 0.92, \"GBP\": 0.82, \"JPY\": 110.0, \"AUD\": 1.35, \"CAD\": 1.25]\n        \n        // Performance test\n        let startTime = Date()\n        for amount in amounts {\n            for (_, rate) in exchangeRates {\n                _ = CurrencyConversionHelper.convert(amount: amount, rate: rate)\n            }\n        }\n        let duration = Date().timeIntervalSince(startTime)\n        \n        // Verify performance\n        print(\"Converted \\(amounts.count * exchangeRates.count) values in \\(duration)s\")\n        #expect(duration < 10.0) // Framework enforces the 1-minute limit\n    }\n    \n    @Test(\"Logging throughput\", .timeLimit(.seconds(30)))\n    func loggingThroughput() async {\n        let iterations = 100_000\n        \n        // No manual measurement needed - framework handles it\n        for i in 0..<iterations {\n            LoggingService.debug(\"Test message \\(i)\")\n        }\n        \n        // Test will fail if it exceeds 30 seconds\n    }\n}\n```\n\nThis prevents runaway tests from blocking CI pipelines. Something I’ve dealt with too many times in XCTest.\n\n## Beyond Basic Conversion: Real Improvements\n\nThe mechanical migration was just the beginning. Here’s where Swift Testing really shines:\n\n### Meaningful Error Testing with #expect(throws:)\n\nStop settling for “it didn’t crash” assertions:\n\n```swift\n// Before: Wishful thinking\ndo {\n    try await manager.startListener()\n} catch {\n    #expect(error != nil) // Well, yes... that's why we're in catch\n}\n\n// After: Actual validation\n#expect(throws: URLError.self) {\n    try await api.fetchWithoutAuth()\n}\n\n// Even better: Test the specific error properties\ndo {\n    try await api.fetchWithoutAuth()\n    Issue.record(\"Expected URLError to be thrown\")\n} catch let error as URLError {\n    #expect(error.code == .notConnectedToInternet)\n}\n```\n\n### Memory Leak Detection Built Right In\n\nSwift Testing’s instance isolation makes leak detection elegant:\n\n```swift\n@Test(\"ThreadSafeBox instances are properly deallocated\")\nfunc threadSafeBoxMemoryLeaks() async throws {\n    weak var weakBox: ThreadSafeBox<String>?\n    \n    do {\n        let box = ThreadSafeBox(\"test-value\")\n        weakBox = box\n        #expect(weakBox != nil, \"Box should be alive within scope\")\n    }\n    \n    await Task.yield() // Allow deallocation\n    #expect(weakBox == nil, \"Box should be deallocated after scope ends\")\n}\n```\n\n### Descriptive Test Names That Tell Stories\n\nNo more cryptic function names:\n\n```swift\n// Before: What does this even test?\n@Test(\"windowControllerManagement\")\n\n// After: Crystal clear intent\n@Test(\"Window controller management handles creation and lifecycle\")\n@Test(\"Position saving and restoration persists window states\")\n@Test(\"AppleScript support methods provide automation capabilities\")\n```\n\n### Advanced Confirmation Patterns\n\nSwift Testing’s [`confirmation()`](https://developer.apple.com/documentation/testing/confirmation\\(_:expectedcount:sourcelocation:_:\\)) handles complex async scenarios elegantly:\n\n```swift\n@Test(\"Multi-step debouncer lifecycle\")\nfunc multiStepDebouncerLifecycle() async throws {\n    try await confirmation(\"Complete lifecycle\", expectedCount: 3) { confirmation in\n        // Step 1: Initial call\n        debouncer.call { confirmation() }\n        try await Task.sleep(for: .milliseconds(70))\n        \n        // Step 2: Second call after first completes\n        debouncer.call { confirmation() }\n        try await Task.sleep(for: .milliseconds(70))\n        \n        // Step 3: Final verification\n        confirmation()\n    }\n}\n```\n\n### Taming Flaky Tests with withKnownIssue\n\nEvery codebase has those intermittent failures. Swift Testing provides a civilized way to handle them with [`withKnownIssue`](https://developer.apple.com/documentation/testing/withknownissue\\(_:isintermittent:sourcelocation:_:\\)):\n\n```swift\n@Test(\"External API availability check\")\nfunc externalAPITest() async throws {\n    await withKnownIssue(\"External API may be temporarily unavailable\", isIntermittent: true) {\n        let url = URL(string: \"https://api.exchangerate-api.com/v4/latest/USD\")!\n        let (_, response) = try await URLSession.shared.data(from: url)\n        let httpResponse = response as! HTTPURLResponse\n        #expect(httpResponse.statusCode == 200, \"Exchange rate API should be available\")\n    }\n}\n```\n\n## The Results\n\nLooking at the final pull requests ([Vibe Meter PR #28](https://github.com/steipete/VibeMeter/pull/28), [Code Looper PR #8](https://github.com/steipete/CodeLooper/pull/8)), the transformation is dramatic:\n\n**Combined Results:**\n\n- **46% fewer test files** (91 → 49 files) through intelligent consolidation\n- **+3,258 lines of enhanced test code** across both projects\n  - Vibe Meter: 61 → 23 files, +1,006 lines\n  - Code Looper: 30 → 26 files, +2,252 lines\n- **Zero test duplication** thanks to parameterized tests\n- **Hierarchical organization** that actually makes sense in Xcode’s navigator\n- **Bulletproof error handling** with specific exception types\n- **CI that doesn’t hang** with proper timeouts on every async test\n\n**Why more lines of code?** Swift Testing prioritizes maintainability over brevity. Here’s a typical transformation:\n\n```swift\n// Before: XCTest (3 lines)\nfunc testCurrencyConversion() {\n    XCTAssertEqual(convert(100, rate: 0.85), 85.0, accuracy: 0.01)\n}\n\n// After: Swift Testing (5 lines)\n@Test(\"Currency conversion with EUR rate\", .tags(.currency))\nfunc currencyConversionEUR() {\n    let result = convert(amount: 100.0, rate: 0.85)\n    #expect(result.isApproximatelyEqual(to: 85.0, tolerance: 0.01))\n}\n```\n\nThe increase comes from descriptive test names, explicit tagging, better parameter naming, and structured test case types for parameterized tests.\n\nThis represents quality improvement, not bloat. Moving from cryptic but compact tests to self-documenting, maintainable test suites.\n\n## Key Takeaways\n\n1. **AI needs guidance**: The initial blind conversion was just the starting point. The real value came from providing structured patterns through the playbook.\n\n2. **Iterative refinement works**: The approach of compile-test-commit-repeat caught issues early and made debugging easier.\n\n3. **Swift Testing encourages better patterns**: Features like parameterized tests and instance isolation naturally lead to cleaner test design.\n\n4. **Migration reveals test quality issues**: This wasn’t just a syntax conversion. It was an opportunity to improve test architecture.\n\n## Additional AI Resources for Swift Testing\n\nFor Cursor users, there’s the [@Docs](https://docs.cursor.com/context/@-symbols/@-docs) context which [includes](https://raw.githubusercontent.com/getcursor/crawler/main/docs.jsonl) the Apple Documentation as a choice.\n\nThere’s also the [Context 7 MCP](https://context7.com/swiftlang/swift-testing) that can be instructed to fetch Apple’s docs for testing, however this only includes code snippets and no explanation. (Enes mentioned that [this is on purpose](https://x.com/enesakar/status/1931296179502927935), though I got great results with explanatory text)\n\nThe combination of AI assistance and systematic refinement made this large-scale migration manageable. While the initial AI conversion provided a foundation, the real value came from applying Swift Testing’s features thoughtfully to create a more maintainable test suite.\n\n## Running Tests Faster with Filters\n\nOne more tip that significantly speeds up the development workflow: use `swift test --filter` to run specific tests instead of the entire suite. This is especially useful when you’re iterating on a particular feature:\n\n```bash\n# Run a single test suite by name\nswift test --filter \"FooTests\"\n\n# Run tests with specific tags\nswift test --filter-tag fast\nswift test --filter-tag currency\n\n# Combine multiple tags (runs tests with ALL specified tags)\nswift test --filter-tag unit --filter-tag fast\n\n# Skip tests with certain tags\nswift test --skip-tag slow\nswift test --skip-tag requiresNetwork\n```\n\nThe performance difference is dramatic when working on a large codebase. Instead of waiting for 700+ tests to run, you can get feedback in seconds by filtering to just the tests you’re working on. Combined with Swift Testing’s parallel execution, this makes the test-driven development cycle incredibly fast.\n\n* * *\n\n**P.S.** - If you’re still manually converting `XCTestExpectation`, stop. [Make AI do it](https://steipete.me/posts/2025/claude-code-is-my-computer). Just give it [better instructions](https://gist.github.com/steipete/84a5952c22e1ff9b6fe274ab079e3a95) than I did.\n",
  "dateCreated": "2026-09-27T19:29:15.293681927+00:00",
  "date_created": "2025-06-06T18:00:00+00:00",
  "date_modified": "2025-06-06T18:00:00+00:00",
  "date_published": "2025-06-06T18:00:00+00:00",
  "description": "How I migrated over 700 tests from XCTest to Swift Testing across two projects, with AI assistance and systematic refinement",
  "external_url": "https://steipete.me/posts/2025/migrating-700-tests-to-swift-testing/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/steipete-me/2025/06/2025-06-06-migrating-700-tests-to-swift-testing-a-real-world-experience.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/steipete-me/2025/06/2025-06-06-migrating-700-tests-to-swift-testing-a-real-world-experience.md"
  },
  "id": "urn:aggr:item:d35499c75b03d5b50605d56db79013a48be345a4",
  "isBasedOn": {
    "@id": "https://steipete.me/posts/2025/migrating-700-tests-to-swift-testing/"
  },
  "mainEntity": {
    "@id": "https://steipete.me/posts/2025/migrating-700-tests-to-swift-testing/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-06-migrating-700-tests-to-swift-testing-a-real-world-experience/#webpage"
    },
    "dateModified": "2025-06-06T18:00:00+00:00",
    "datePublished": "2025-06-06T18:00:00+00:00",
    "headline": "Migrating 700+ Tests to Swift Testing: A Real-World Experience",
    "timeRequired": "PT11M",
    "url": "https://steipete.me/posts/2025/migrating-700-tests-to-swift-testing/",
    "wordCount": 2282
  },
  "name": "Archived snapshot: Migrating 700+ Tests to Swift Testing: A Real-World Experience",
  "preview": null,
  "reading_minutes": 11,
  "replicated_at": null,
  "source": "steipete-me",
  "source_name": "Peter Steinberger",
  "summary": "How I migrated over 700 tests from XCTest to Swift Testing across two projects, with AI assistance and systematic refinement",
  "tags": [],
  "temporalCoverage": "2026-09-27T19:29:15.293681927+00:00",
  "title": "Migrating 700+ Tests to Swift Testing: A Real-World Experience",
  "url": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-06-migrating-700-tests-to-swift-testing-a-real-world-experience/",
  "word_count": 2282
}