‹ 首页

swift-testing

@notque · 收录于 1 周前

Swift testing: XCTest, Swift Testing framework, async patterns.

适合你,如果你在用 Swift 写代码并需要测试覆盖

/ 下载安装
swift-testing.skill双击,或拖进 Claude 桌面版 / Cowork,即完成安装↓ .skill↓ .zip
用别的 agent?下载 .zip 解压,把文件夹放进它的技能目录
Claude Code~/.claude/skills/(项目级 .claude/skills/)
Codex CLI~/.codex/skills/
Cursor自动读取上面两处目录
其他工具见其文档的「skills」目录;两个下载是同一份文件,只是名字不同
/ 通过 npx 安装 校验哈希
npx oh-my-skill add notque/vexjoy-agent/swift-testing
/ 通过 bash 安装
curl -fsSL https://oh-my-skill.com/install.sh | bash -s -- notque/vexjoy-agent/swift-testing
/ 已经装过?验证本机副本,不用重装
npx oh-my-skill verify notque/vexjoy-agent/swift-testing
安装目标可用 --agent / --scope 或 --to 明确指定;省略时只会在唯一已存在的 agent 目录上自动选择,零命中或多命中会停止并提示。content_hash 缺失或不一致均拒装。
407GitHub stars
待重算上下文体积
镜像托管

怎么用

技能原文 SKILL.md作者撰写 · MIT · f27fa0e

Swift Testing Patterns

XCTest Basics

XCTest is Apple's foundational testing framework. Every test class inherits from XCTestCase and uses setUp/tearDown for lifecycle management.

import XCTest
@testable import MyApp

final class UserServiceTests: XCTestCase {
    var sut: UserService!
    var mockStore: MockUserStore!

    override func setUp() {
        super.setUp()
        mockStore = MockUserStore()
        sut = UserService(store: mockStore)
    }

    override func tearDown() {
        sut = nil
        mockStore = nil
        super.tearDown()
    }

    func testFetchUser_withValidID_returnsUser() {
        mockStore.stubbedUser = User(id: "1", name: "Alice")

        let user = sut.fetchUser(id: "1")

        XCTAssertNotNil(user)
        XCTAssertEqual(user?.name, "Alice")
    }

    func testFetchUser_withInvalidID_returnsNil() {
        mockStore.stubbedUser = nil

        let user = sut.fetchUser(id: "unknown")

        XCTAssertNil(user)
    }
}
Swift Testing Framework (Swift 5.9+)

The Swift Testing framework replaces XCTest with a more expressive, macro-driven approach. Use @Test for individual tests, #expect for assertions, and @Suite for grouping.

import Testing
@testable import MyApp

@Suite("User Service")
struct UserServiceTests {
    let mockStore = MockUserStore()

    @Test("fetches user by valid ID")
    func fetchValidUser() {
        mockStore.stubbedUser = User(id: "1", name: "Alice")
        let service = UserService(store: mockStore)

        let user = service.fetchUser(id: "1")

        #expect(user?.name == "Alice")
    }

    @Test("returns nil for unknown ID")
    func fetchUnknownUser() {
        mockStore.stubbedUser = nil
        let service = UserService(store: mockStore)

        #expect(service.fetchUser(id: "unknown") == nil)
    }
}
Parameterized Tests

Swift Testing supports parameterized tests natively, eliminating boilerplate for table-driven patterns.

@Test("validates email formats", arguments: [
    ("alice@example.com", true),
    ("bob@", false),
    ("", false),
    ("valid+tag@sub.domain.com", true),
])
func emailValidation(email: String, isValid: Bool) {
    #expect(EmailValidator.isValid(email) == isValid)
}
Async Testing
Async Test Methods (XCTest)

XCTest supports async test methods directly. For callback-based APIs, use XCTestExpectation.

// Direct async/await support
func testFetchProfile_async() async throws {
    let service = ProfileService(client: MockHTTPClient())

    let profile = try await service.fetchProfile(userID: "1")

    XCTAssertEqual(profile.name, "Alice")
}

// Callback-based APIs with expectations
func testFetchProfile_callback() {
    let expectation = expectation(description: "Profile fetched")
    let service = ProfileService(client: MockHTTPClient())

    service.fetchProfile(userID: "1") { result in
        switch result {
        case .success(let profile):
            XCTAssertEqual(profile.name, "Alice")
        case .failure(let error):
            XCTFail("Unexpected error: \(error)")
        }
        expectation.fulfill()
    }

    waitForExpectations(timeout: 5)
}
Async Tests with Swift Testing
@Test("fetches profile asynchronously")
func fetchProfileAsync() async throws {
    let service = ProfileService(client: MockHTTPClient())

    let profile = try await service.fetchProfile(userID: "1")

    #expect(profile.name == "Alice")
}
UI Testing

UI tests use XCUIApplication to interact with the app as a user would. Always prefer accessibility identifiers over text matching for resilient tests.

final class LoginUITests: XCTestCase {
    let app = XCUIApplication()

    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        app.launchArguments = ["--uitesting"]
        app.launch()
    }

    func testSuccessfulLogin() {
        let emailField = app.textFields["login.emailField"]
        let passwordField = app.secureTextFields["login.passwordField"]
        let loginButton = app.buttons["login.submitButton"]

        emailField.tap()
        emailField.typeText("alice@example.com")

        passwordField.tap()
        passwordField.typeText("password123")

        loginButton.tap()

        let welcomeLabel = app.staticTexts["home.welcomeLabel"]
        XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 5))
        XCTAssertEqual(welcomeLabel.label, "Welcome, Alice")
    }
}

Set accessibility identifiers in production code:

emailTextField.accessibilityIdentifier = "login.emailField"
passwordTextField.accessibilityIdentifier = "login.passwordField"
submitButton.accessibilityIdentifier = "login.submitButton"
Test Doubles: Protocol-Based Mocking

Swift's protocol-oriented design makes mocking straightforward. Define dependencies as protocols, then provide mock implementations in tests.

// Production protocol
protocol HTTPClient {
    func data(from url: URL) async throws -> (Data, URLResponse)
}

// Production implementation
struct URLSessionHTTPClient: HTTPClient {
    let session: URLSession

    func data(from url: URL) async throws -> (Data, URLResponse) {
        try await session.data(from: url)
    }
}

// Test double
final class MockHTTPClient: HTTPClient {
    var stubbedData: Data = Data()
    var stubbedResponse: URLResponse = HTTPURLResponse()
    var capturedURLs: [URL] = []

    func data(from url: URL) async throws -> (Data, URLResponse) {
        capturedURLs.append(url)
        return (stubbedData, stubbedResponse)
    }
}
Dependency Injection

Inject dependencies through initializers to make classes testable:

final class ProfileService {
    private let client: HTTPClient

    init(client: HTTPClient) {
        self.client = client
    }

    func fetchProfile(userID: String) async throws -> Profile {
        let url = URL(string: "https://api.example.com/users/\(userID)")!
        let (data, _) = try await client.data(from: url)
        return try JSONDecoder().decode(Profile.self, from: data)
    }
}
Key Conventions
  • One assertion per concept -- a single test can have multiple assertions if they verify the same logical behavior, but avoid testing unrelated things together.
  • Arrange-Act-Assert -- structure every test into setup, execution, and verification phases.
  • Name tests descriptively -- testFetchUser_withExpiredToken_throwsAuthError is better than testFetch2.
  • Prefer Swift Testing for new code -- use @Test and #expect when targeting Swift 5.9+; fall back to XCTest for older targets or UI tests.
  • Ensure test independence -- each test must be runnable in isolation; always produce self-contained test state.
按 MIT 许可原样转载,未经改动 · 在 GitHub 查看 →

评论

登录即可评论;带「已验证安装」的,是发布者名下有本店的安装或持有记录。