Avoiding force unwrapping in Swift unit tests
Discover page available: Unit TestingWhile force unwrapping (using !
) is an important Swift feature that would be hard to work without (especially when interacting with other languages, such as C and Objective-C), using it also generally leads to code that’s less safe and more prone to runtime errors. Essentially, we’re trading runtime safety for compile-time convenience.
So avoiding force unwrapping (when possible) can help us build apps that are more stable and that give us better error messages when something does go wrong, but what about when writing tests? Dealing with optionals and unknown types in a safe way can require quite a lot of code, so the question is whether we want to do all of that additional work when writing tests as well? That is what we'll take a look at this week — let’s dive in!
Testing code vs production code
When working with tests, we often make a clear distinction between our testing code and our production code. While it’s important to keep both of those two code bases separate (after all, we don’t want to accidentally ship our mocks as part of our App Store builds), it’s not necessarily a distinction we should use when talking about code quality.
If we think about it, what are some of the reasons that we want to maintain a high set of quality standards for the code that we ship to our users?
- We want our app to be stable and run smoothly for our users.
- We want to make our app easy to maintain and modify in the future.
- We want to make it easy to onboard new people onto our team.
Now, if we instead think about our tests, what are some of the things that we want to avoid?
- Tests that are unstable, flaky and hard to debug.
- Tests that are time-consuming to maintain and update when new features get added to our app.
- Tests that are hard to understand for new people that join our team.
You might see where I’m going with this.
For the longest time I used to treat testing code as something that I’d just quickly put together because someone told me that I had to write tests. I didn’t care much about their quality, because I saw them as a chore that I actually didn’t want to do in the first place.
However, once I started seeing first-hand just how much quicker I could verify my code using tests, and how much more confident I became that by code was actually working — my attitude towards testing started to change.
So these days I do believe that it’s important that we hold our testing code to the same high standards as our shipping production code. Since our test suite is something that we have to constantly update and maintain, we should make it as easy as possible to do so.
The problem with force unwrapping
So what does all of this have to do with force unwrapping in Swift?
While force unwrapping is indeed necessary sometimes, it’s easy to make it a ”go-to solution” when writing tests. Let’s take a look at an example in which we’re writing a test to verify that the login mechanism of a UserService
works as expected:
class UserServiceTests: XCTestCase {
func testLoggingIn() {
// Setup a mock to always return a successful response
// for the login endpoint
let networkManager = NetworkManagerMock()
networkManager.mockResponse(forEndpoint: .login, with: [
"name": "John",
"age": 30
])
// Setup a service and login
let service = UserService(networkManager: networkManager)
service.login(withUsername: "john", password: "password")
// Now we want to make assertions based on the logged in user,
// which is an optional, so we force unwrap it
let user = service.loggedInUser!
XCTAssertEqual(user.name, "John")
XCTAssertEqual(user.age, 30)
}
}
As you can see above, we force unwrap our service’s loggedInUser
before making assertions on it. While doing something like the above is not necessarily wrong, it can lead to some problems down the line if this test starts failing for some reason.
Let’s say that someone (and remember, “someone” often means “your future self” in this context) makes a change within our app’s networking code which causes the above test to break. If that happens, the only error message that will be available will currently be the following:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
While that might not be a big problem when working locally in Xcode (since that error will be displayed inline), it can become quite problematic if the error is thrown within our app’s Continuous Integration process. In that situation, the above error message might appear within a big “wall of text”, which can make it really hard to figure out where it came from. Further, it will prevent any subsequent tests from being executed (since the test process will crash), which can make it really slow and annoying to work on a fix.
Guard and XCTFail
One potential solution to the above problem is to simply use the guard
statement to gracefully unwrap the optional in question, and to then call XCTFail()
if that fails, like this:
guard let user = service.loggedInUser else {
XCTFail("Expected a user to be logged in at this point")
return
}
While doing the above is a valid approach in some situations, I really recommend avoiding it — since it adds control flow within our tests. For stability and predictability, we typically want our tests to follow a simple given, when, then structure, where all of our statements are executed at the topmost level, and adding control flow completely breaks that by introducing multiple code branches.
Sticking with optionals
Another approach is to let our optionals remain optional. For some use cases that totally works, including our UserManager
example. Since we are performing assertions against the logged in user’s name
and age
, we will automatically get an error if any of those properties end up being nil
. If we also throw in an additional XCTAssertNotNil
check against the user
itself, we’ll end up with a pretty solid test with great diagnostics:
let user = service.loggedInUser
XCTAssertNotNil(user, "Expected a user to be logged in at this point")
XCTAssertEqual(user?.name, "John")
XCTAssertEqual(user?.age, 30)
Now if our test starts failing, we’ll get the following information:
XCTAssertNotNil failed - Expected a user to be logged in at this point
XCTAssertEqual failed: ("nil") is not equal to ("Optional("John")")
XCTAssertEqual failed: ("nil") is not equal to ("Optional(30)")
That makes it a lot easier to understand what went wrong and what we need to do in order to debug and fix the issue. Big win!
Throwing tests
A third option that’s really useful in some situations is to replace APIs that return optionals with throwing ones. The beauty of throwing APIs is that they can super easily be used as optional ones when needed, so in many cases we’re not sacrificing any usability by opting for the throwing approach. For example, let’s say we have an EndpointURLFactory
(that creates URLs for certain endpoints within our app) that currently returns an optional:
class EndpointURLFactory {
func makeURL(for endpoint: Endpoint) -> URL? {
...
}
}
Let’s now convert it into a throwing API instead, like this:
class EndpointURLFactory {
func makeURL(for endpoint: Endpoint) throws -> URL {
...
}
}
Now, all that we need to do when we still want an optional URL is to call our method using the try?
keyword:
let loginEndpoint = try? urlFactory.makeURL(for: .login)
The big advantage of doing the above is that we can now simply use try
within our tests, and the XCTest runner itself will automatically handle any invalid values for us. It’s a bit of a hidden gem, but Swift tests can actually be throwing functions — check this out:
class EndpointURLFactoryTests: XCTestCase {
func testSearchURLContainsQuery() throws {
let factory = EndpointURLFactory()
let query = "Swift"
// Since our test function is throwing, we can simply use 'try' here
let url = try factory.makeURL(for: .search(query))
XCTAssertTrue(url.absoluteString.contains(query))
}
}
No optionals, no force unwrapping, and excellent diagnostics in case something starts failing.
Requiring optionals
However, not all APIs can be converted from returning optionals to become throwing. But it turns out that XCTest also provides a way to conditionally unwrap any optional as a throwing operation, by using the XCTUnwrap
function. If an optional turned out to be nil
, then an error will automatically be thrown.
Let’s go back to the first UserManager
example. Instead of either having to force unwrap loggedInUser
, or treat it as an optional, we could now simply do this:
let user = try XCTUnwrap(service.loggedInUser)
XCTAssertEqual(user.name, "John")
XCTAssertEqual(user.age, 30)
Really cool! Using the XCTUnwrap
function we can get rid of a lot of force unwrapping — without making our tests harder to write, or harder to follow.
Conclusion
Treating our test code with the same amount of care as our app code can feel somewhat awkward at first, but doing so can make it much easier to maintain our tests in the long run, especially when it comes to error messages and diagnostics.
The only time that I always use force unwrapped optionals in test code is when setting up properties in test cases. Since these will always be created in setUp
and removed in tearDown
, I don’t think it’s worth defining them as true optionals. Like always, you have to take a look at your own code and apply your own preferences, to see what tradeoffs you think are worth making.
What do you think? Will you apply some of the techniques from this post in your test code, or do you already use something similar? Let me know, along with any questions, comments or feedback you might have, on Twitter @johnsundell.
Thanks for reading! 🚀