Just wondering if there is a more succinct syntax than what I'm doing below when trying to test Errors when the Error type has associated types?
Specifically anything better I could be doing than the switch statements when all I want to know is if it is that one subtype?
extension TestingErrors.ExampleError:Equatable {}
@Suite("Test Errors")
struct TestingErrors {
enum ExampleError:Error {
case unknownError(_ message: String)
case codedError(_ code:Int)
case noExtras
}
func throwsMessage(_ message:String) throws {
throw ExampleError.unknownError(message)
}
func throwsCode(_ codeToThrow:Int) throws {
throw ExampleError.codedError(codeToThrow)
}
func throwsSimple() throws {
throw ExampleError.noExtras
}
@Test func simpleErrorTest() async throws {
//required explicit equatable
#expect(throws: ExampleError.noExtras) {
try throwsSimple()
}
}
@Test func testForExpectedCode() async throws {
let expectedCode = 42
//when you know the exact code, everything is easy.
await #expect(throws: ExampleError.codedError(expectedCode)) {
try await throwsCode(expectedCode)
}
//but if it is in a range?
await #expect {
try await throwsCode(expectedCode)
} throws: { error in
guard let error = error as? ExampleError else {
return false
}
switch error {
case .codedError(let code):
return (20...49).contains(code)
default:
return false
}
}
}
@Test func testForAnyCode() async throws {
//Can't find the right way to frame this? Is it possible?
// #expect(throws: ExampleError.codedError(_)) {
// try await throwsCode(Int.random(in: 0...5))
// }
await #expect {
try await throwsCode(Int.random(in: 0...5))
} throws: { error in
guard let error = error as? ExampleError else {
return false
}
switch error {
case .codedError(_):
return true
default:
return false
}
}
}
@Test func testForMessage() async throws {
let messageToThrow = "well that was a big oops"
let messageShouldContain = "oops"
//Would have been nice but a big ask..
// await #expect(throws: ExampleError.unknownError(.contains(messageShouldContain))) {
// try await throwsMessage(messageToThrow)
// }
await #expect {
try await throwsMessage(messageToThrow)
} throws: { error in
guard let error = error as? ExampleError else {
return false
}
switch error {
case .unknownError(let message):
return message.contains(messageShouldContain)
default:
return false
}
}
}
}
grynspan
(Jonathan Grynspan)
May 18, 2025, 8:33pm
2
If your error type conforms to Equatable, you can pass an instance of it to compare against exactly instead of passing just the error's type. For more complex comparisons, try something like:
let error = #expect(throws: MyError.self) {
try foo()
}
if let error {
#expect(error.property == 123)
/* ... */
}
It is not syntactically valid to write #expect(case ...)
because case
statements are not expressions.
2 Likes
Thanks! That does let me tighten it up quite a bit.
enum ExampleError:Error {
case unknownError(_ message: String)
case codedError(_ code:Int)
case noExtras
//would be a pain to add for a lot of them, but could be worse.
func isCodedError() -> Bool {
switch self {
case .codedError(_):
return true
default:
return false
}
}
}
@Test func testForExpectedCodeV2() async throws {
let expectedCode = 42
let error = await #expect(throws: ExampleError.self) {
try await throwsCode(expectedCode)
}
#expect(error != nil)
#expect(error == .codedError(expectedCode))
}
@Test func testForAnyCodeV2() async throws {
let error = await #expect(throws: ExampleError.self) {
try await throwsCode(Int.random(in: 0...5))
}
#expect(error?.isCodedError() == true)
}
grynspan
(Jonathan Grynspan)
May 18, 2025, 9:22pm
4
You can simplify your code even further by using let error = try #require(throws: ...)
. It will throw a (different) error on failure, so its result is non-optional.
2 Likes
Danny
May 18, 2025, 11:28pm
5
There are several missing features which make your code have to look terrible:
is case
An overload of #expect
which takes a () -> Bool
instead of a Bool
.
Swift Testing's lack of incorporation of typed throws.*
@Suite("Test Errors")
struct TestingErrors {
enum ExampleError: Error & Equatable {
case unknownError(_ message: String)
case codedError(_ code:Int)
case noExtras
}
func `throw`(message: String) throws(ExampleError) {
throw .unknownError(message)
}
func `throw`(code: Int) throws(ExampleError) {
throw .codedError(code)
}
func `throw`() throws(ExampleError) {
throw .noExtras
}
@Test func simpleErrorTest() throws {
#expect(throws: ExampleError.noExtras, performing: `throw`)
}
@Test func testForExpectedCode() throws {
let expectedCode = 42
#expect(throws: ExampleError.codedError(expectedCode)) {
try `throw`(code: expectedCode)
}
#expect({
guard case .codedError(let code) = (#expect(throws: ExampleError.self) {
try `throw`(code: expectedCode)
}) else { return false }
return (20...49).contains(code)
} ())
}
@Test func testForAnyCode() throws {
#expect({
guard case .codedError = (#expect(throws: ExampleError.self) {
try `throw`(code: .random(in: 0...5))
}) else { return false }
return true
}() )
}
@Test func testForMessage() throws {
#expect({
guard case .unknownError(let message) = (#expect(throws: ExampleError.self) {
try `throw`(message: "well that was a big oops")
}) else { return false }
return message.contains("oops")
} ())
}
}
* This is the least important; typed throws feel like they're another abandoned feature at this point, and require redundant annotation which is worse than providing a metatype.
expect {
guard case .unknownError(let message) = (expectThrows { () throws(ExampleError) in
try `throw`(message: "well that was a big oops")
}) else { return false }
return message.contains("oops")
}
func expectThrows<Error>(performing expression: () throws(Error) -> some Any) -> Error? {
#expect(throws: Error.self, performing: expression)
}
func expect(_ condition: () -> Bool) {
#expect(condition())
}
swhitty
(Simon Whitty)
May 19, 2025, 12:32am
6
TIL that the thrown, typed error is returned! So valuable, but I don't believe this is visible at all from the documentation?
grynspan
(Jonathan Grynspan)
May 19, 2025, 12:34am
7
It is documented. It's new in Swift 6.1.
1 Like
swhitty
(Simon Whitty)
May 19, 2025, 12:43am
8
Looks like some of the See Also
links on other pages are incorrect but yes, you are right — found it thank you .
grynspan
(Jonathan Grynspan)
May 19, 2025, 12:46am
9
I'm not sure what you're referring to. Can you point me at the bad links and we can make sure to get them corrected?
swhitty
(Simon Whitty)
May 19, 2025, 12:54am
10
The see also section on this page: (entered via google search so probably old but its not clear)
grynspan
(Jonathan Grynspan)
May 19, 2025, 1:21am
11
Thanks! That section indeed appears to be out of date. @smontgomery or @suzannaratcliff would you please let our DevPub colleagues know?
I'm pretty new to swift-testing and it's pretty new so I'm not sure I follow everything you're saying but between your and grynspan's tips I'm ending up with something that doesn't feel that terrible to me!
Maybe I have low standards ;)
I even got a self contained function that I might move up to the Suite level out of it! Thanks!
Now to replace that random with parameterization!
#expect(errorContainedIn(range:20...49, with:Int.random(in: 20...49), from: throwsCode))
func errorContainedIn(range:ClosedRange<Int>, with:Int,
from function:(Int) async throws -> Void) -> Bool {
//note: build fails with require
guard case .codedError(let code) = (#expect(throws: ExampleError.self) {
try function(with)
}) else { return false }
return range.contains(code)
}
ETA: see also Comparing enum cases while ignoring associated values - #24 by tera
I went back and watched last year's "Going Further" WWDC video before I launched in to porting some of my tests, TBH
Thanks for pointing out the other docs below!
1 Like