I have a fairly reasonable task: I want to write some test code to ensure that certain paths of my code are throwing errors, but not only that, errors of a certain "type".
OK… this sounds like it should be really trivial to do.
Here's the setup:
enum MyErrors : ErrorType {
case Basic
case MoreInfo(title: String, description: String)
}
func f(value: Int) throws {
switch value {
case 0:
throw MyErrors.Basic
case 1:
throw MyErrors.MoreInfo(title: "A title?", description: "1s are bad, k?")
default:
break
}
}
And for the tests:
func testFThrowsOn0() {
do {
try f(0)
XCTFail("This was supposed to throw")
}
catch MyErrors.Basic {}
catch {
XCTFail("Incorrect error thrown")
}
}
func testFThrowsOn1() {
do {
try f(1)
XCTFail("This was supposed to throw")
}
catch MyErrors.MoreInfo {}
catch {
XCTFail("Incorrect error thrown")
}
}
func testFDoesNotThrowOn2() {
do {
try f(2)
}
catch {
XCTFail("This was not supposed to throw")
}
}
Ok, the tests do what they are supposed to do… but that is some ugly code. What I want to write is this:
func testFThrowsOn0() {
XCTAssertDoesThrowErrorOfType(try f(0), MyErrors.Basic)
}
func testFThrowsOn1() {
XCTAssertDoesThrowErrorOfType(try f(1), MyErrors.MoreInfo)
}
func testFDoesNotThrowOn2() {
XCTAssertDoesNotThrow(try f(2))
}
I have no idea how to write this code… the simple version XCTAssertDoesThrow
is trivial, just catch any exception and perform the logic. However… how to pass in the value, especially on an enum with associated values, so that it can be properly pattern matched, I don't even know if that's possible.
The only way I know how to even come close to what I want to is to cheat significantly.
enum MyErrors : ErrorType {
case Basic
case MoreInfo //(title: String, description: String)
}
func == (lhs: ErrorType, rhs: ErrorType) -> Bool {
return lhs._code == rhs._code
}
func != (lhs: ErrorType, rhs: ErrorType) -> Bool {
return !(lhs == rhs)
}
func XCTAssertDoesThrowErrorOfType(@autoclosure fn: () throws -> (),
message: String = "", type: ErrorType, file: String = __FILE__,
line: UInt = __LINE__)
{
do {
try fn()
XCTFail(message, file: file, line: line)
}
catch {
if error != type { XCTFail(message, file: file, line: line) }
}
}
So I had to:
- Get ride of my associated enum value, which means I'll need to wrap this information into a struct.
- Define the
==
and!=
operators to compareErrorType
through a hack that exposed the_code
value that is used for bridging to ObjC with
NSError
the oridinal value of the case statement. - Define the
XCTAssertDoesThrowErrorOfType
function.
This allows me to achieve most of what I wanted, but it came at a cost and some really hacky code that is likely going to break in future betas of Swift 2.0.
So really I'm back at square one:
- Try the
do-try-catch
boilerplate code each time. - Catch all errors and report that as a test success.
- Write the really hack code.
I think option #3 is a no-go. So out of convenience and hope that we'll be able to solve this better at a later date, I'm likely to go with option #2. BUT this leaves a test hole…
Any other ideas?
Update – Added the 4th option I failed to mention…
I forgot about the fourth option: promote each of the enum case values to its own type. Though… that has some significant drawbacks as well.
struct MyBasicError : ErrorType {
let _code: Int
let _domain: String
}
struct MoreInfoError : ErrorType {
let _code: Int
let _domain: String
let title: String
let description: String
}
First, we need to add the _code
and _domain
workarounds. That sucks…
And now we want a type signature like this:
func XCTAssertDoesThrowErrorOfType<T : ErrorType>(@autoclosure fn: () throws -> (),
message: String = "", type: T, file: String = __FILE__,
line: UInt = __LINE__)
However… since Swift cannot specialize the generic call, that means we need to pass an actual instance into the function for T
or switch to use T.self
and T.Type
as the parameter. That's also less than ideal. So let's just do this:
func XCTAssertDoesThrowErrorOfType(@autoclosure fn: () throws -> (),
message: String = "", type: MirrorType, file: String = __FILE__,
line: UInt = __LINE__)
{
do {
try fn()
XCTFail(message, file: file, line: line)
}
catch {
if reflect(error).summary != type.summary {
XCTFail(type.summary, file: file, line: line)
}
}
}
At least the callsite doesn't need an instance anymore:
XCTAssertDoesThrowErrorOfType(try f(0), type: reflect(MyBasicError))
The plus side is that this is work for both structs and enums, so long as the enum only has a single case that you care about comparing.
Anyhow… still stuck on how to actually do this in a "proper" way or if we'll be able to in Swift 2.0.
Conclusion
It seems that, at least for now, the reflect()
solution is the best I can come up with. It works for both enums and structs, and it can be extended to consider the details of each of those if desired. The only real drawback is associated enums: you need to pass in an instance of that enum (you could pass in strings but that exposed an implementation detail and removes the ability to further inspect the types).
XCTAssertDoesThrowErrorOfType(try f(0), type: reflect(EnumError.Info(title: "")))