You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Swift Testing中优雅断言含关联值的枚举?

Swift Testing 中优雅的枚举Case断言方案

问题描述

我定义了如下枚举:

enum State: Equatable {
    case active
    case paused(for: TimeInterval)
}

在Swift Testing中,无关联值的枚举case可以直接用简洁断言:#expect(newState == .active);带关联值的全相等断言也能这么写:#expect(newState == .paused(for: 42.1))。但如果只想仅断言枚举case的身份(不关心关联值),直接写会报错,找不到简洁的实现方式。

非断言场景里可以用if case .paused(_) = newState做模式匹配,但放到Swift Testing里就得结合Issue.record自定义错误信息,写法繁琐还不好读;要是想对关联值做带容差的断言,写法会更复杂,逻辑还容易搞反。

我希望能有类似RSpec里这种简洁的断言方式:

expect(new_state).to be_enum_of_type(State.paused)
  .with_values(for: a_value_within(0.001).of(77.1))

请问在Swift Testing中,有没有更优雅、符合框架风格的枚举case断言方法?


解决方案

方案1:自定义封装断言函数

基于Swift Testing的现有能力,封装一组针对枚举的断言函数,实现简洁调用的同时保留框架风格:

import SwiftTesting

extension Test {
    // 仅断言枚举case匹配
    func expect<Enum>(_ enumValue: Enum, matchesCase casePattern: (Enum) -> Bool, file: StaticString = #file, line: UInt = #line) {
        guard casePattern(enumValue) else {
            Issue.record("Expected enum case to match, but got \(enumValue)", file: file, line: line)
            return
        }
    }
    
    // 针对State的带容差关联值断言
    func expect(_ state: State, isPausedWithin tolerance: TimeInterval, of expected: TimeInterval, file: StaticString = #file, line: UInt = #line) {
        guard case let .paused(for: actual) = state else {
            Issue.record("Expected .paused case, but got \(state)", file: file, line: line)
            return
        }
        guard abs(actual - expected) <= tolerance else {
            Issue.record("Expected paused time within \(tolerance) of \(expected), but got \(actual)", file: file, line: line)
            return
        }
    }
}

使用示例:

// 仅断言case身份
expect(newState, matchesCase: { case .paused(_) = $0 })

// 带容差的关联值断言
expect(newState, isPausedWithin: 0.001, of: 77.1)

方案2:自定义Matcher(贴合框架原生风格)

Swift Testing支持自定义Matcher协议,可以实现通用的枚举匹配器,适配任意枚举类型,完全遵循框架设计逻辑:

import SwiftTesting

// 通用枚举case匹配器
struct EnumCaseMatcher<Enum>: Matcher where Enum: Equatable {
    private let casePredicate: (Enum) -> Bool
    private let caseLabel: String
    
    init(predicate: @escaping (Enum) -> Bool, label: String) {
        self.casePredicate = predicate
        self.caseLabel = label
    }
    
    func matches(_ actual: Enum, in context: inout MatcherContext) -> Bool {
        guard casePredicate(actual) else {
            context.recordIssue("Expected enum case to be \(caseLabel), but got \(actual)")
            return false
        }
        return true
    }
}

// 给Matcher扩展便捷构造方法
extension Matcher where Self == EnumCaseMatcher<Enum>, Enum: Equatable {
    static func `case`(_ predicate: @escaping (Enum) -> Bool, label: String) -> EnumCaseMatcher<Enum> {
        EnumCaseMatcher(predicate: predicate, label: label)
    }
    
    // State枚举专属便捷方法
    static func active() -> EnumCaseMatcher<State> {
        .case({ $0 == .active }, label: ".active")
    }
    
    static func paused() -> EnumCaseMatcher<State> {
        .case({ case .paused(_) = $0 }, label: ".paused")
    }
}

// 带容差的关联值匹配器
struct PausedToleranceMatcher: Matcher where Value == State {
    private let expected: TimeInterval
    private let tolerance: TimeInterval
    
    init(expected: TimeInterval, tolerance: TimeInterval) {
        self.expected = expected
        self.tolerance = tolerance
    }
    
    func matches(_ actual: State, in context: inout MatcherContext) -> Bool {
        guard case let .paused(for: actualTime) = actual else {
            context.recordIssue("Expected .paused case, but got \(actual)")
            return false
        }
        guard abs(actualTime - expected) <= tolerance else {
            context.recordIssue("Expected paused time \(expected) ± \(tolerance), but got \(actualTime)")
            return false
        }
        return true
    }
}

extension Matcher where Self == PausedToleranceMatcher, Value == State {
    static func paused(withTolerance expected: TimeInterval, tolerance: TimeInterval) -> PausedToleranceMatcher {
        PausedToleranceMatcher(expected: expected, tolerance: tolerance)
    }
}

使用示例(完全贴合Swift Testing原生语法):

// 仅断言case
#expect(newState, .paused())
#expect(newState, .active())

// 带容差的关联值断言
#expect(newState, .paused(withTolerance: 77.1, tolerance: 0.001))

方案对比

  • 自定义断言函数:实现简单,适合快速适配特定枚举类型;
  • 自定义Matcher:更贴合框架原生逻辑,可复用性强,适合多枚举测试场景或通用断言需求。

内容的提问来源于stack exchange,提问作者Robin Daugherty

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 13:31:22