Add swift-testing skill with framework basics

This commit is contained in:
Michael 2026-01-07 20:01:27 -06:00
parent 6fe317b4d3
commit 7cd86cf0eb
2 changed files with 282 additions and 0 deletions

127
swift-testing/SKILL.md Normal file
View file

@ -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.

View file

@ -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
```