#expect(throws:)からthrowされたエラーを返す
Return errors from #expect(throws:)
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Swift Testing には、コードが期待どおりエラーを throw するかを確認するための #expect(throws:) と #require(throws:) のオーバーロードが用意されています。これらには次の3種類があります。
- エラーの型を渡し、その型のエラーが
throwされたかをチェックするもの Equatableに適合したエラーのインスタンスを渡し、それと等しいエラーがthrowされたかをチェックするもの- 末尾クロージャで任意の検証ロジックを書けるもの
問題は3番目のオーバーロードのエルゴノミクスです。クロージャに渡されるエラーは any Error 型なので、まず具体的な型へキャストしないと中身を調べられません。さらに、クロージャは「マッチした」を true、「マッチしなかった」を false として返さなくてはならず、論理が分かりづらく、簡潔にも書きにくくなっていました。
try #require {
let potato = try Sack.randomPotato()
try potato.turnIntoFrenchFries()
} throws: { error in
guard let error = error as PotatoError else {
return false
}
guard case .potatoNotPeeled = error else {
return false
}
return error.variety != .russet
}
このようなとき、検証クロージャの中で #expect() を使いたくなりますが、#expect() は必要な真偽値を返してくれませんし、本来は1件のはずのテスト失敗が複数の issue として記録されてしまうこともあります。
02 どのように解決されるのか
エラー型やエラーのインスタンスを受け取るオーバーロードを変更し、成功時に throw されたエラーを戻り値として返すようにします。あわせて、検証クロージャを受け取る問題のオーバーロードは非推奨にします。
これにより、throw されたエラーをそのまま受け取って、その後の #expect() で個別の条件を素直にチェックできるようになります。any Error からのキャストも不要です。
let error = try #require(throws: PotatoError.self) {
let potato = try Sack.randomPotato()
try potato.turnIntoFrenchFries()
}
#expect(error == .potatoNotPeeled)
#expect(error.variety != .russet)
戻り値の仕様
#expect(throws:) のオーバーロードは、期待どおりのエラーが throw された場合はそのエラーを返し、失敗した場合は nil を返すオプショナルになります。#require(throws:) のオーバーロードは非オプショナルでエラーを返し、失敗した場合は従来どおり ExpectationFailedError を throw します。
@discardableResult
@freestanding(expression) public macro expect<E, R>(
throws errorType: E.Type,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation,
performing expression: () async throws -> R
) -> E? where E: Error
@discardableResult
@freestanding(expression) public macro require<E, R>(
throws errorType: E.Type,
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation,
performing expression: () async throws -> R
) -> E where E: Error
Equatable なエラーのインスタンスを渡すオーバーロードも同様に、E? または E を返すように変更されます。なお、#require(throws:) 自身が失敗時に throw する ExpectationFailedError は、戻り値として返されることはありません。#require(throws:) を #expect(throws:) の代わりに使う意味がなくなってしまうためです。
検証クロージャ版の非推奨化
任意の検証ロジックを書けるオーバーロードは、(any Error)? あるいは any Error を返すように変更されたうえで非推奨となります。これからは #expect(throws:) / #require(throws:) の戻り値を使って検証することが推奨されます。
@available(swift, deprecated: 100000.0, message: "Examine the result of '#expect(throws:)' instead.")
@discardableResult
@freestanding(expression) public macro expect<R>(
_ comment: @autoclosure () -> Comment? = nil,
sourceLocation: SourceLocation = #_sourceLocation,
performing expression: () async throws -> R,
throws errorMatcher: (any Error) async throws -> Bool
) -> (any Error)?
任意の throw されたエラーをそのまま受け取りたい場合は、エラー型として (any Error).self を渡せば、これまでと同じように any Error 型でエラーを得られます。
ソース互換性に関する注意
戻り値が追加されたことで、既存コードでも次の2つのパターンで新たに警告が出る場合があります。
ひとつは、マクロ呼び出しの戻り値の型が外側のクロージャの戻り値型を推論する材料となり、その値が捨てられているケースです。たとえば次のコードでは、withUnsafePointer(to:_:) の戻り値が未使用であるという警告が出ます。
withUnsafePointer(to: potato) { pPotato in
// ⚠️ Result of call to 'withUnsafePointer(to:_:)' is unused
#expect(throws: PotatoError.rotten) {
try pokePotato(pPotato)
}
}
この警告は、マクロ呼び出しの結果か関数呼び出しの結果を _ に代入することで抑制できます。
withUnsafePointer(to: potato) { pPotato in
_ = #expect(throws: PotatoError.rotten) {
try pokePotato(pPotato)
}
}
もうひとつは、#require(throws:) をジェネリックな文脈で使い、エラー型のジェネリックパラメータが Never に解決されてしまうケースです。この場合は返すべき値が存在しないため、Swift Testing は実行時に「API Misused」の issue を記録し、#expect(throws:) を使うか Never.self を渡さないように促します。
03 今後の見通し
将来の構想として、これらのマクロのシグネチャに typed throws を採用し、テスト対象のコードが throw するエラー型を静的に要求できるようにすることが挙げられています。実現を約束するものではありません。
ただし、現状のままシグネチャに typed throws を採用すると、テスト対象のコード側にも typed throws の採用を強制してしまうという課題があります。たとえば次のコードはコンパイルできなくなってしまいます。
func cook(_ food: consuming some Food) throws { ... }
let error: PotatoError? = #expect(throws: PotatoError.self) {
var potato = Potato()
potato.fossilize()
try cook(potato) // 🛑 ERROR: Invalid conversion of thrown error type
// 'any Error' to 'PotatoError'
}
マクロやその展開をオーバーロードすることで、上のコードもそのままコンパイルでき、意図どおりに動作させられる可能性があるとされており、将来の Proposal で改めて typed throws の採用が検討される見込みです。