diff --git a/swift-testing/SKILL.md b/swift-testing/SKILL.md new file mode 100644 index 0000000..bab9e6d --- /dev/null +++ b/swift-testing/SKILL.md @@ -0,0 +1,127 @@ +--- +name: swift-testing +description: Swift Testing framework (@Test, @Suite, #expect, #require) for Swift 5.9+/Xcode 16+. Use when writing tests, migrating from XCTest, or fixing Swift Testing failures. +--- + +# Swift Testing + +## Overview + +Write unit tests using Apple's Swift Testing framework with `@Test`, `@Suite`, `#expect`, and `#require`. Replaces XCTest for non-UI tests. + +## Quick reference + +| XCTest | Swift Testing | +|--------|---------------| +| `import XCTest` | `import Testing` | +| `class FooTests: XCTestCase` | `@Suite struct FooTests` | +| `func testBar()` | `@Test func bar()` | +| `XCTAssertEqual(a, b)` | `#expect(a == b)` | +| `XCTAssertNil(x)` | `#expect(x == nil)` | +| `XCTAssertThrowsError` | `#expect(throws:)` | +| `XCTUnwrap(x)` | `try #require(x)` | +| `setUpWithError()` | `init() throws` | +| `tearDown()` | `deinit` | + +## Workflow + +### 1. Set up the test suite + +Create a struct with `@Suite` and use `init()` for setup: + +```swift +import Testing + +@Suite struct UserServiceTests { + let sut: UserService + let mockRepo: MockRepository + + init() { + mockRepo = MockRepository() + sut = UserService(repository: mockRepo) + } +} +``` + +### 2. Write test functions + +Add `@Test` to test functions. Remove the `test` prefix: + +```swift +@Test func fetchUser_returnsValidUser() async throws { + let user = try await sut.fetchUser(id: "123") + #expect(user.name == "John") +} +``` + +### 3. Use assertions + +- `#expect(condition)` — soft assertion, test continues on failure +- `try #require(optional)` — hard assertion, test stops if nil + +```swift +#expect(result == expected) +#expect(isValid) +let value = try #require(optional) + +#expect(throws: ValidationError.self) { + try validate(badInput) +} +``` + +### 4. Add parameterized tests + +Run the same test with multiple inputs: + +```swift +@Test(arguments: ["", " ", " "]) +func validate_rejectsBlankStrings(_ input: String) { + #expect(!isValid(input)) +} + +@Test(arguments: [ + (input: "hello", expected: 5), + (input: "", expected: 0) +]) +func count_returnsCorrectLength(input: String, expected: Int) { + #expect(input.count == expected) +} +``` + +### 5. Apply traits for test control + +```swift +@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil)) +func onlyOnCI() { } + +@Test(.disabled("Waiting for backend fix")) +func brokenEndpoint() { } + +@Test(.tags(.slow, .network)) +func networkHeavyTest() { } + +@Test(.timeLimit(.minutes(1))) +func mustCompleteFast() { } + +@Suite(.serialized) +struct DatabaseTests { + @Test func insert() { } + @Test func delete() { } +} +``` + +## Common mistakes + +| Mistake | Fix | +|---------|-----| +| `#expect(x == nil)` on `T??` | Use `try #require(x)` first to unwrap outer optional | +| Test not running | Ensure `@Test` attribute is present | +| Shared state between tests | Use `init()` for setup, avoid `static var` | +| Async test not awaited | Mark test `async`, use `await` | +| XCTest assertions in Swift Testing | Replace with `#expect` / `#require` | + +## Reference material + +- See `references/swift-testing-basics.md` for core concepts and syntax. +- See `references/migration-from-xctest.md` for migration strategies. +- See `references/advanced-patterns.md` for parameterized tests, traits, and mocking. diff --git a/swift-testing/references/swift-testing-basics.md b/swift-testing/references/swift-testing-basics.md new file mode 100644 index 0000000..187c37f --- /dev/null +++ b/swift-testing/references/swift-testing-basics.md @@ -0,0 +1,155 @@ +# Swift Testing Basics + +## Core Components + +### @Test + +```swift +@Test func userCanLogin() { } +@Test func emptyInput_returnsError() { } +@Test("User can update their profile") func updateProfile() { } +``` + +**Requirements:** +- Must be a function (not a property or subscript) +- Can be `async` and/or `throws` +- Can be instance method or free function +- No parameters unless using parameterized testing + +### @Suite + +```swift +@Suite struct AuthenticationTests { + @Test func login() { } + @Test func logout() { } +} +``` + +**Suite features:** +- Can be `struct`, `class`, `actor`, or `enum` +- `struct` recommended (value semantics, fresh instance per test) +- Can nest suites for hierarchy +- Supports traits for suite-wide configuration + +### #expect + +```swift +#expect(value == 42) +#expect(array.isEmpty) +#expect(string.contains("hello")) +#expect(!isDisabled) +``` + +Soft assertion—test continues on failure. + +**Comparisons:** +```swift +#expect(a == b) +#expect(a != b) +#expect(a > b) +#expect(a === b) +``` + +**Errors:** +```swift +#expect(throws: (any Error).self) { + try riskyOperation() +} + +#expect(throws: NetworkError.self) { + try fetch() +} + +#expect { + try validate("") +} throws: { error in + error as? ValidationError == .empty +} + +#expect(throws: Never.self) { + try safeOperation() +} +``` + +### #require + +Hard assertion—test stops on failure. Use for unwrapping. + +```swift +let user = try #require(await fetchUser()) +#expect(user.name == "John") + +try #require(array.count > 0) +let first = array[0] + +let viewModel = try #require(controller.viewModel as? ProfileViewModel) +``` + +## Lifecycle + +```swift +@Suite struct ServiceTests { + let service: MyService + let mockRepository: MockRepository + + init() { + mockRepository = MockRepository() + service = MyService(repository: mockRepository) + } + + @Test func fetchData() async { } +} +``` + +`init()` runs before each test. Each test gets a fresh instance. + +### Teardown (class only) + +```swift +@Suite class ResourceTests { + var tempFile: URL? + + init() throws { + tempFile = try createTempFile() + } + + deinit { + if let tempFile { + try? FileManager.default.removeItem(at: tempFile) + } + } +} +``` + +## Async + +```swift +@Test func asyncFetch() async throws { + let data = try await api.fetchData() + #expect(data.count > 0) +} +``` + +```swift +@Test(.timeLimit(.seconds(5))) +func mustCompleteFast() async { } + +@Suite(.serialized) +struct OrderDependentTests { + @Test func first() { } + @Test func second() { } +} +``` + +## Running Tests + +**Xcode:** Cmd+U (all), click diamond (individual) + +**Command line:** +```bash +swift test +swift test --filter UserTests +swift test --filter .tags:slow +swift test --skip .tags:slow +``` +