The Problem
While dabbling along with swift I encountered a situation that I was unsure how to handle. I am creating a new class. This class has two properties: text: String
and images: [UIImage]
. Both properties are optional, so long as the other is set. Meaning that we can have text
, or images
or both, but we can’t have neither.
My class structure looks something like this:
import UIKit
public class QQComponent: NSObject {
private(set) var text: String = ""
private(set) var images: [UIImage] = []
init(let text: String) {
self.text = text
}
init(let images: [UIImage]) {
self.images = images
}
init(let text: String, let images: [UIImage]) {
self.text = text
self.images = images
}
}
Which I of course had started by writing the unit tests:
func testComponentWithText() {
let question = "What color is the sky?"
let comp = QQComponent(text: question)
XCTAssertEqual(question, comp.text, "text going into comp does not come out the same")
}
func testComponentWithImage() {
let images: [UIImage] = [
UIImage(named: "frog")!
]
let comp = QQComponent(images: images)
XCTAssertEqual(comp.images, images, "Images go in, but do not come out the same")
}
func testComponentWithTextAndImages() {
let images: [UIImage] = [
UIImage(named: "frog")!
]
let question = "What color is this frog?"
let comp = QQComponent(text: question, images: images)
}
Then I realized that I wasn’t checking the exceptional cases: No text when using the text-only constructor, and no images when using the images only constructor. Theoretically, the text and images constructor should also check that at least one of the two is set, just in case.
Since there are no exceptions in swift, I wasn’t sure how to handle this.
After some discussion with the friendly devs on IRC I came up with 2 alternatives.
Option ‘A’: Optional init()
The first solution I came across is to make my constructor return an optional value. Meaning that if we don’t get the input we wanted, we return nil
.
Here’s what that looks like:
init?(let text: String) {
super.init()
if text.isEmpty { return nil }
self.text = text
}
Make sure you call super.init()
before returning nil
Notice how I call super.init()
before returning. This is very important, if you miss this minor detail you will land on a compile error: All stored properties of a class instance must be initialized before returning nil from an initializer
. And the worst part about it is that this vague message is really referring to the properties of NSObject
.
The unit test is updated as follows (for testing the text
-only init()
):
func testComponentWithText() {
let question = "What color is the sky?"
let comp = QQComponent(text: question)!
XCTAssertEqual(question, comp.text, "text going into comp does not come out the same")
}
In this test I forcefully unwrap the QQComponent
by using the !
. This is because I’m expecting a value back, and since it’s my unit test it’s probably okay for it to crash if there’s actually a problem.
To test the exceptional case, I add a new test method:
func testComponentTextWithNoText() {
let question = ""
let comp = QQComponent(text: question)?
XCTAssertNil(comp, "Must have text when using text init")
}
Here I use an XCTAssertNil
to determine if I get a value back or not. So basically, if I decide to use an optional init, I have to unwrap my objects nicely, checking for nils everywhere.
Pros
- Gives us a way to check if our object is created successfully or not when an exceptional case is possible
Cons
- In practice, it does not tell us why something may have gone wrong in creating our object. Note: it tells us in this case because we are making an assertion in our unit tests, but the outside world may never see these
- We have to check for
nil
everywhere.
Option ‘B’: use assertions
Another possible solution is to use an assertion in my constructor. This assertion will work in development only and cause the app to crash, but it will expose to us developers exactly what went wrong.
Here’s what the new init(text: String)
looks like:
init(let text: String) {
assert(!text.isEmpty,
"text cannot be empty when using text only constructor, received: '\(text)'")
self.text = text
}
Remove the optional unwrappings from your previous test cases (the !
and ?
):
func testComponentWithText() {
// This is an example of a functional test case.
let question = "What color is the sky?"
let comp = QQComponent(text: question)
XCTAssertEqual(question, comp.text, "text going into comp does not come out the same")
}
func testComponentTextWithNoText() {
let question = ""
let comp = QQComponent(text: question)
XCTAssertNil(comp, "Must have text when using text init")
}
Run the tests. Crash!
assertion failed: text cannot be empty when using text only constructor, received: '':
Pros
- Specific messaging to indicate there was a problem.
- Actually handling exceptional behavior exceptionally.
- Instead of checking for
nil
objects in our return value, now we check what we pass to ourinit()
. Not sure if this is pro or con, but whatever.
Cons
- No way to unit test
Well. That’s the disadvantage. There’s no way to catch this assertion (or at least that I’m aware of) but some people have mentioned adding try-catch to swift.