如何在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
相关产品推荐
相关产品推荐

