agentsclimarketplace

Swift testing

Skill Tyr0/agent-skills/plugins/swift-expert/skills/swift-testing

Use this skill whenever the user asks about testing in Swift, including the Swift Testing framework (@Suite, @Test, #expect, #require), parameterized tests, async test support, test organization with tags, or migrating from XCTest to Swift Testing. Also use it for questions about writing unit tests, integration tests, mocking dependencies, test coverage, or test performance in Swift. Triggers on 'how do I write a Swift test', 'what is @Test', 'how does #expect work', 'how do I test async code', 'Swift Testing vs XCTest', or any question about testing Swift code.From its SKILL.md

Install
npx -y skills add Tyr0/agent-skills --skill swift-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.6 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it

Swift Testing Reference

A dense reference for the Swift Testing framework (Swift 5.9+, Xcode 16+) — the modern replacement for XCTest.

Requires: Swift 5.9+ / Xcode 16+. Import Testing. XCTest remains available; the two frameworks can coexist in the same target.


Core Concepts

ConceptSwift TestingXCTest
Test function@Test func name()func testName()
Test class/struct@Suite struct Nameclass Name: XCTestCase
Assertion#expect(expr)XCTAssert(expr)
Required assertion#require(expr)XCTUnwrap(expr) / XCTAssertNotNil
Fatal assertion#require throws on failureXCTFail()
Setupinit()setUp()
TeardowndeinittearDown()
Skipping#expect throws: or withKnownIssueXCTSkip

Basic Test Structure

import Testing

@Suite("User authentication")
struct AuthTests {
    let service: AuthService

    init() {
        service = AuthService(database: .inMemory)
    }

    @Test("Valid credentials return a session token")
    func loginWithValidCredentials() throws {
        let token = try service.login(email: "[email protected]", password: "correct")
        #expect(token.isNotEmpty)
    }

    @Test("Invalid password throws AuthError.invalidCredentials")
    func loginWithInvalidPassword() throws {
        #expect(throws: AuthError.invalidCredentials) {
            try service.login(email: "[email protected]", password: "wrong")
        }
    }
}
  • Suites can be struct, class, or actorstruct is idiomatic (value semantics, fresh instance per test).
  • Every @Test function gets a fresh instance of the suite — no shared mutable state between tests.
  • init() runs before each test; deinit runs after (use for resource cleanup).

#expect — Non-Fatal Assertions

#expect records a failure but continues test execution. Use for independent assertions where you want to see all failures.

#expect(value == 42)
#expect(array.isEmpty)
#expect(string.hasPrefix("Hello"))
#expect(optionalValue != nil)

// Custom failure message
#expect(result == expected, "Expected \(expected) but got \(result)")

// Assertion on thrown errors
#expect(throws: MyError.specificCase) {
    try riskyOperation()
}

// Assert no error is thrown
#expect(throws: Never.self) {
    try safeOperation()
}

Failure output includes the full expression tree — no need to write custom messages for most cases:

Expectation failed: (value → 41) == 42

#require — Fatal Assertions

#require throws on failure, stopping the test immediately. Use when subsequent assertions depend on this value.

@Test func tokenIsValid() throws {
    let response = try fetchToken()

    // Stop if token is nil — no point continuing
    let token = try #require(response.token)

    // These only run if token was non-nil
    #expect(token.expiresAt > Date.now)
    #expect(token.value.count == 32)
}

#require also works as an unwrapping operator for optionals:

let user = try #require(findUser(id: 42))   // throws if nil
#expect(user.name == "Alice")

Async Tests

Mark the test async — Swift Testing handles the execution context automatically:

@Test("Fetches user from network")
func fetchUser() async throws {
    let user = try await userService.fetch(id: 42)
    #expect(user.name == "Alice")
}

Async suites with shared async state:

@Suite
actor DatabaseTests {
    var db: Database!

    init() async throws {
        db = try await Database.inMemory()
        try await db.migrate()
    }

    @Test func insertsRecord() async throws {
        try await db.insert(User(name: "Alice"))
        let count = try await db.count(User.self)
        #expect(count == 1)
    }
}

Use actor suite when multiple tests share async mutable state and need protection from data races.


Parameterized Tests

Run a single test with multiple inputs. Eliminates copy-paste test duplication.

@Test("Validates email format", arguments: [
    "[email protected]",
    "[email protected]",
])
func validEmailFormats(email: String) {
    #expect(EmailValidator.isValid(email))
}

@Test("Rejects malformed emails", arguments: [
    "",
    "no-at-sign",
    "@no-local-part.com",
    "spaces [email protected]",
])
func invalidEmailFormats(email: String) {
    #expect(!EmailValidator.isValid(email))
}

Two-argument parameterized tests

@Test("Converts units correctly", arguments: [
    (1.0, UnitLength.meters, 100.0, UnitLength.centimeters),
    (1.0, UnitLength.kilometers, 1000.0, UnitLength.meters),
])
func unitConversion(value: Double, from: UnitLength, expected: Double, to: UnitLength) {
    let result = Measurement(value: value, unit: from).converted(to: to).value
    #expect(result == expected)
}

Zip vs product

// zip: pairs are (inputs[0], outputs[0]), (inputs[1], outputs[1]) — same count required
@Test(arguments: zip(inputs, expectedOutputs))
func pairedTest(input: String, expected: String) { ... }

// product (default with two collections): all combinations
@Test(arguments: operators, operands)
func allCombinations(op: String, n: Int) { ... }

Tags

Tags group tests across suites and enable filtering in Xcode or swift test.

extension Tag {
    @Tag static var networking: Self
    @Tag static var slow: Self
    @Tag static var critical: Self
}

@Suite(.tags(.networking))
struct NetworkTests {
    @Test(.tags(.slow)) func largeDownload() { ... }
    @Test(.tags(.critical)) func authFlow() { ... }
}

Run tagged tests:

swift test --filter .tags(.networking)

Known Issues / Expected Failures

@Test("This known bug is being tracked in #1234")
func knownBug() {
    withKnownIssue {
        #expect(buggyFunction() == expected)  // expected failure; test passes
    }
}

If the issue is fixed and the test starts passing, withKnownIssue reports an unexpected pass — prompting you to remove it.


Traits

Traits customize how tests run:

@Test(.disabled("Not implemented yet"))
func futureFeature() { }

@Test(.timeLimit(.minutes(1)))
func networkRequest() async { }

@Suite(.serialized)  // run tests in this suite serially (not in parallel)
struct OrderDependentTests { }

Dependency Injection and Mocking

Inject dependencies through init() — no global mutable state needed:

protocol HTTPClient {
    func fetch(_ url: URL) async throws -> Data
}

struct MockHTTPClient: HTTPClient {
    let response: Data
    func fetch(_ url: URL) async throws -> Data { response }
}

@Suite
struct WeatherServiceTests {
    let service: WeatherService

    init() {
        service = WeatherService(client: MockHTTPClient(response: mockWeatherJSON))
    }

    @Test func parsesCurrentTemperature() async throws {
        let weather = try await service.current(for: "Boulder, CO")
        #expect(weather.temperature == 18.5)
    }
}

XCTest Migration

You do not need to migrate all at once — both frameworks coexist in the same target.

XCTestSwift Testing
class MyTests: XCTestCase@Suite struct MyTests
func testFoo()@Test func foo()
XCTAssertEqual(a, b)#expect(a == b)
XCTAssertNil(x)#expect(x == nil)
XCTAssertNotNil(x)#expect(x != nil) or try #require(x)
XCTAssertThrowsError(try f())#expect(throws: Error.self) { try f() }
XCTUnwrap(optional)try #require(optional)
XCTSkip("reason")try #require(Bool(false), "reason") or .disabled trait
setUp()init()
tearDown()deinit
XCTExpectFailurewithKnownIssue { }

Do not mix XCTest and Swift Testing in the same test class/suite. Keep them in separate files.

@MainActor is not needed on Swift Testing suites — the framework handles concurrency correctly without it.


Anti-Patterns

Anti-patternProblemFix
Using XCTAssert* in @Test functionsXCTest assertions don't integrate with Swift Testing's failure modelUse #expect / #require exclusively in @Test functions
Shared mutable state between tests via static varTests become order-dependent; parallel execution causes racesUse struct suites — each test gets a fresh instance
#expect for nil-unwrapping before dependent assertionsTest continues with a nil crash instead of clean failure messageUse try #require(optional) to stop early on nil
No @Suite groupingTests are hard to filter and navigateGroup related tests into named suites
@MainActor on an entire @SuiteSerializes all tests on the main actor unnecessarilyOnly annotate specific @Test functions that touch MainActor state
Parameterized test with one argumentAdds boilerplate for no gainUse a regular @Test with one hardcoded value; parameterize when there are 3+ cases
Keeping dead XCTest alongside migrated Swift TestingTwo frameworks, double maintenanceMigrate file-by-file; delete XCTest file when Swift Testing equivalent is complete

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,750. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.