$npx -y skills add superagents-lab/xcode27-skills --skill test-modernizerModernize test suites to use modern Swift Testing features or migrate from XCTest.
| 1 | # Test Modernizer |
| 2 | |
| 3 | Apply when: user asks to modernize, update, migrate, supercharge, or convert their tests. |
| 4 | XCTest should be migrated to Swift Testing when possible, existing Swift Testing tests should be evaluated to see if they could be better structured adopting newer features. |
| 5 | |
| 6 | Do not apply when: user asks to write new tests from scratch (without existing XCTest code), user asks about XCTest features only, user only asks about |
| 7 | test results or test running, user is asking to update tests to cover new functionality rather than updating the tests themselves, |
| 8 | user is debugging test failures without mentioning migration, user has UI automation tests using XCUI* APIs (these cannot be migrated to Swift Testing). |
| 9 | |
| 10 | ## Migration Reference |
| 11 | |
| 12 | ### Imports |
| 13 | |
| 14 | Replace `import XCTest` with `import Testing`. A file can import both if it contains mixed test content during incremental migration. |
| 15 | |
| 16 | When removing import XCTest, check whether the file uses Foundation types (URL, CharacterSet, ProcessInfo, Data, etc.). XCTest re-exports |
| 17 | Foundation, so add `import Foundation` if needed. |
| 18 | |
| 19 | ### Test Classes to Suites |
| 20 | |
| 21 | Remove `XCTestCase` inheritance. Prefer `struct` over `class`: |
| 22 | |
| 23 | - `final class FoodTruckTests: XCTestCase { ... }` -> `struct FoodTruckTests { ... }` |
| 24 | |
| 25 | ### setUp/tearDown to init/deinit |
| 26 | |
| 27 | Replace `override func setUp()` with `init()` (can be `async throws`). Replace `override func tearDown()` with `deinit`. If `deinit` is needed, use |
| 28 | `actor` or `final class` instead of `struct` (since structs have no `deinit`). Change stored properties to not use implicitly-unwrapped optional |
| 29 | types, and move their initial assignment from `setUp` to either be initialized inline or, if the initialization is complex, in an initializer. |
| 30 | |
| 31 | ``` |
| 32 | struct MyTests { |
| 33 | var fixture = Fixture() |
| 34 | mutating func `Fixture behaves as expected`() { |
| 35 | #expect(fixture.doSomething()) |
| 36 | } |
| 37 | } |
| 38 | ``` |
| 39 | |
| 40 | Avoid pulling instance variables into function bodies; this can cause noise. Swift Testing reinvokes the initializer fresh before each test runs. |
| 41 | If the test mutates an instance variable with value semantics, you may need to mark the test function `mutating`. |
| 42 | |
| 43 | ### Test Methods |
| 44 | |
| 45 | Replace the `test` name prefix with the `@Test` attribute. If the resulting test name includes multiple camelCase words, |
| 46 | use a raw identifier with the test name in sentence case. |
| 47 | |
| 48 | - `func testEngineDoesNotStall() { ... }` -> `@Test func `Engine does not stall`() { ... }` |
| 49 | - `func testIgnition() { ... }` -> `@Test func ignition() { ... }` |
| 50 | |
| 51 | Test functions can be `async`, `throws`, or `async throws`, and can be isolated to a global actor with `@MainActor`. |
| 52 | |
| 53 | ### Assertions to Expectations |
| 54 | |
| 55 | When migrating a test from XCTest to Swift Testing, apply these mappings: |
| 56 | |
| 57 | `XCTAssert(x)`, `XCTAssertTrue(x)` -> `#expect(x)` |
| 58 | `XCTAssertFalse(x)` -> `#expect(!x)` |
| 59 | `XCTAssertNil(x)` -> `#expect(x == nil)` |
| 60 | `XCTAssertNotNil(x)` -> `#expect(x != nil)` |
| 61 | `XCTAssertEqual(x, y)` -> `#expect(x == y)` |
| 62 | `XCTAssertNotEqual(x, y)` -> `#expect(x != y)` |
| 63 | `XCTAssertIdentical(x, y)` -> `#expect(x === y)` |
| 64 | `XCTAssertNotIdentical(x, y)` -> `#expect(x !== y)` |
| 65 | `XCTAssertGreaterThan(x, y)` -> `#expect(x > y)` |
| 66 | `XCTAssertGreaterThanOrEqual(x, y)` -> `#expect(x >= y)` |
| 67 | `XCTAssertLessThanOrEqual(x, y)` -> `#expect(x <= y)` |
| 68 | `XCTAssertLessThan(x, y)` -> `#expect(x < y)` |
| 69 | `try XCTUnwrap(x)` -> `try #require(x)` |
| 70 | |
| 71 | There is no direct equivalent for `XCTAssertEqual(_:_:accuracy:)`; use floating point math directly. |
| 72 | |
| 73 | ### Errors |
| 74 | |
| 75 | When the error type is `Equatable` and the exact value is known, prefer to check the specific error value. |
| 76 | |
| 77 | ``` |
| 78 | XCTAssertThrowsError(try f()) |
| 79 | ``` |
| 80 | -> |
| 81 | ``` |
| 82 | #expect(throws: (any Error).self) { |
| 83 | try f() |
| 84 | } |
| 85 | ``` |
| 86 | |
| 87 | ``` |
| 88 | XCTAssertThrowsError(try f()) { error in |
| 89 | XCTAssertEqual(error, specificError) |
| 90 | } |
| 91 | ``` |
| 92 | -> |
| 93 | ``` |
| 94 | #expect(throws: specificError) { |
| 95 | try f() |
| 96 | } |
| 97 | ``` |
| 98 | |
| 99 | ``` |
| 100 | XCTAssertThrowsError(try f()) { error in |
| 101 | // Check error |
| 102 | } |
| 103 | ``` |
| 104 | -> |
| 105 | ``` |
| 106 | let error = #expect(throws: (any Error).self) { |
| 107 | try f() |
| 108 | } |
| 109 | // Check error |
| 110 | ``` |
| 111 | |
| 112 | ``` |
| 113 | XCTAssertNoThrow(try f()) |
| 114 | ``` |
| 115 | -> |
| 116 | ``` |
| 117 | #expect(throws: Never.self) { |
| 118 | try f() |
| 119 | } |
| 120 | ``` |
| 121 | |
| 122 | ### continueAfterFailure |
| 123 | |
| 124 | By default `continueAfterFailure` is true, which means expectations do not halt the test run. |
| 125 | Some XCTestCases set `continueAfterFailure = false`, which means the `XCTAssert` family of functions |
| 126 | will throw Objective-C exceptions that halt the test execution. |
| 127 | |
| 128 | When a test method sets `continueAfterFailure = false`, all subsequent assertions need to be `try #require(x)` |
| 129 | instead of `#expect(x)` to preserve this behavior. When adding `try #require(x)`, add `throws` to the affected methods. |
| 130 | |
| 131 | When `continueAfterFailure = false` is set in `setUp`, the conversion to `try #require(x)` must apply |
| 132 | to **all assertions in all test methods** in that class. |
| 133 | |
| 134 | ### Promote `Issue.record`/`XCTFail` to expectations |
| 135 | |
| 136 | Wherever it is not disruptive, convert usage of `Issue.record` or `XCTFail` |