.st0{fill:#FFFFFF;}

How to Unit Test Optionals in Swift 

 September 29, 2020

by  Jon Reid

You have an optional value. How can you write an XCTest assertion to verify it, while getting the most from any assertion failure?

Two core features of the Swift programming language are:

  • Optional values, which may or may not be present; and
  • Enumeration cases with associated values.

In this post, we’ll look at different ways to test optionals. Another post will examine how to test enums.

Test the Value of an Optional

Some functions return a single optional value. Here’s a computed property that returns an optional String. In this example, it’s a hard-coded value.

var fruitInHand: String? { "apple" }

When you expect the result to be nil, use XCTAssertNil:

func test_nil() {
    let result = fruitInHand
    XCTAssertNil(result)
}

This fails with the following message. Make sure it looks okay.

XCTAssertNil failed: "apple"

When designing a test, always force it to fail. You want to see the failure message, because designing clear failures is an important part of designing the test. Ask yourself:

  • Does the message describe the expectation?
  • Does the message describe the actual result? It should provide enough detail to give us contextual clues about where the incorrect value may have come from.

When designing a test, always force it to fail. You want to see the failure message.

Click to Tweet

What if you expect the result to be non-nil? There’s XCTAssertNotNil, but it’s a pretty weak assertion. You usually know the specific value you want. So instead, let’s use XCTAssertEqual:

func test_equalityWithOptional() {
    let result = fruitInHand
    XCTAssertEqual(result, "orange")
}

This fails with the following message:

XCTAssertEqual failed: ("Optional("apple")") is not equal to ("Optional("orange")")

As you can see, we didn’t need to define the expected value “orange” as an optional String. Swift wants both sides of an equality to be the same type, so it promoted the string literal to an optional for us.

Test All Values of an Optional with Properties

What if a function returns an optional struct or class? For this example, let’s use a struct with a few properties.

struct Person {
    let id: String
    let middleName: String?
    let isZombie: Bool
}

Let’s also make a couple of computed properties which return an optional Person.

var nobody: Person? { nil }

var noMiddleName: Person? {
    Person(id: "KEY", middleName: nil, isZombie: false)
}

We’ll use these to examine different ways to test the optional results.

If we want to compare the entire Person, we can make it Equatable:

struct Person: Equatable {

Once a type is Equatable, we can use XCTAssertEqual.

func test_checkEntirePerson() {
    let result = noMiddleName
    XCTAssertEqual(
        result,
        Person(id: "KEY", middleName: "Fitzgerald", isZombie: false)
    )
}

This works. But the test is looking more complicated, because we have to create an entire Person to compare against. Now let’s see what the failure message looks like.

XCTAssertEqual failed: ("Optional(BlogTests.Person(id: "KEY", middleName: nil, isZombie: false))") is not equal to ("Optional(BlogTests.Person(id: "KEY", middleName: Optional("Fitzgerald"), isZombie: false))")

How quickly can you find the mismatch? And this only has three simple properties!

Relying on the type being Equatable looks simple. But it can get complicated:

  • As you can see, the failure output makes it hard to find the difference. (However, you can get better messages by using Krzysztof Zabłocki’s Difference library.)
  • Some types are difficult to make Equatable. This happens if one of the properties or sub-properties isn’t also Equatable.

Test One Property of an Optional

A particular test may only care about one of the properties. Sweeping all properties into a single assertion can lead to fragile tests. They can become fragile when there’s a mismatch on a property, when that property doesn’t matter for the test case.

Also, including all properties can confuse the person reading the test. It makes it hard to tell which value is the important one.

“Fragile Test”  is one of the test smells described in the bible of unit testing, xUnit Test Patterns by Gerard Meszaros.

(This is an affiliate link. If you buy anything, I earn a commission, at no extra cost to you.)


Let’s look at ways to examine one property when the container is optional. This is more than “How do I access the property of an optional in Swift code in general?” Remember, we’re focusing on unit test assertions. We want to minimize the work, but maximize the usefulness of failure messages. 

Method 1: Force-Unwrapping

Swift programmers strive to avoid force-unwrapping (the dreaded “!”) in production code. After all, the beauty of optionals is that they help us avoid impossible situations which cause crashes.

But should we hold the same standard for test code? Well, it depends.

If the optional is returned by the System Under Test, then avoid force-unwrapping. When the container is nil, we don’t want test execution to crash. It should report the mismatch and continue to the next test.

But if the optional is provided by the test code, then force-unwrapping is okay. The test is providing the data, so it can make assumptions about the nature of that data. This typically happens if the test decodes JSON examples, using them as Test Fixtures. That is, they’re used to create the object graph of the System Under Test, or passed as input to it.

Method 2: Optional Chaining

Optional chaining with ? may or may not succeed, so it returns an optional. Is it suitable for tests? That depends on whether the underlying property is optional.

Let’s start with a test that’s only interested in the id property, which is non-optional. Given an optional Person, here’s how you can use optional chaining to access and verify the property:

func test_mustHaveID() {
    let result = nobody
    XCTAssertEqual(result?.id, "KEY")
}

This fails with the following message:

XCTAssertEqual failed: ("nil") is not equal to ("Optional("KEY")")

That’s pretty clear. The id property itself isn’t optional, so the nil value is there because result is nil.

But what about an optional property? Let’s copy this test but try to examine the middleName property, which is an optional String

func test_mustHaveMiddleName_inconclusive() {
    let result = nobody
    XCTAssertEqual(result?.middleName, "Fitzgerald")
}

The result of the optional chaining is nil. Is it because result is nil? Or is there a valid result, but middleName is nil?

So optional chaining works well enough… unless the property itself is also optional. We want the failure to tell us where the chain became nil.

Method 3: XCTUnwrap

When we’re examining an optional property of an optional value, we want to craft the test to tell us just what’s going on. Basically, we need to drill down from the outer result to the property we want, but the test may fail before we reach the property.

A good way to do that is to use XCTUnwrap to unwrap the optional result. If result is nil, the test will fail. Now XCTUnwrap can throw an exception. So to use it, we need to do two things:

  • Precede it with try
  • Declare the test case as throws
func test_mustHaveMiddleName() throws {
    let result = try XCTUnwrap(nobody)
    XCTAssertEqual(result.middleName, "Fitzgerald")
}

This time, the failure reads as follows:

XCTUnwrap failed: expected non-nil value of type "Person"

This message is crystal clear. And if there is a Person, it will go into the non-optional result and continue on to compare its middleName against the expected value.

Testing an Optional Bool

To test Boolean values, we normally use XCTAssertTrue and XCTAssertFalse. But what if it’s an optional Bool? Let’s start from another computed property:

var maybe: Bool? { nil }

How would you assert that this value should be true, or be false? At first, I used the nil-coalescing operator ?? to use the opposite value:

func test_optionalBool_coalesceToFail() {
    let result = maybe
    XCTAssertTrue(result ?? false)
}

This does fail, as it should. But the failure message is less than helpful:

XCTAssertTrue failed

Did it fail because result was false, or because it was nil? We can’t tell.

So let’s try a couple of approaches. One is to use what we already know about asserting on optional values: we can just use XCTAssertEqual.

func test_optionalBool_equality() {
    let result = maybe
    XCTAssertEqual(result, true)
}

This time, we get more information in the failure message:

XCTAssertEqual failed: ("nil") is not equal to ("Optional(true)")

So for optional Boolean values, one approach is to use XCTAssertEqual instead of XCTAssertTrue or XCTAssertFalse. There are three values to consider—true, false, and nil—and the failure message will tell us the actual value.

Another approach is when the Bool comes as a property inside an optional struct or class. The Person type we’ve been using has an isZombie Boolean property. We can use XCTUnwrap on the optional container, followed by a regular old Boolean assertion.

func test_shouldBeZombie() throws {
    let result = try XCTUnwrap(nobody)
    XCTAssertTrue(result.isZombie)
}

So, Which One?

Do I have a recommendation of which approach is best? I hesitate to lay down a rule of thumb, because tools have different purposes, and different strengths. I’m also not keen on “we must have one way” because I’m interested in outcomes, not dogma. Choose whatever works well in its particular situation.

That said, tools also evolve. I’ve been leaning on optional chaining as the easiest way to examine a single property. But it doesn’t report well when the property itself is optional. Meanwhile, XCTUnwrap (available as of Xcode 11) produces the clearest failure messages.

I wouldn’t retroactively go back and update old tests, unless I’m actively working in them. But is there any downside to using XCTUnwrap across the board? The only cost I see is no big deal: annotating that your tests can throw exceptions. To make that even simpler, I’ve updated my free test-oriented code snippets to declare new test cases as throws. Sign up to get these snippets today.

__CONFIG_group_edit__{}__CONFIG_group_edit__
__CONFIG_local_colors__{"colors":{"2d90f":"Royal Blue","559d9":"Link Water","00527":"Link Water","3313f":"Bunker","2410f":"Link Water"},"gradients":{}}__CONFIG_local_colors__

The Definitive Guide from Pragmatic Programmers

My book iOS Unit Testing by Example: XCTest Tips and Techniques Using Swift is the definitive guide to unit testing iOS apps. It covers foundational tools and skills, testing specific behaviors of iOS apps, and how to use the fast feedback from your tests.

__CONFIG_colors_palette__{"active_palette":0,"config":{"colors":{"55c7c":{"name":"Main Accent","parent":-1}},"gradients":[]},"palettes":[{"name":"Default Palette","value":{"colors":{"55c7c":{"val":"var(2d90f)"}},"gradients":[]},"original":{"colors":{"55c7c":{"val":"rgb(19, 114, 211)","hsl":{"h":210,"s":0.83,"l":0.45}}},"gradients":[]}}]}__CONFIG_colors_palette__
Learn More
__CONFIG_colors_palette__{"active_palette":0,"config":{"colors":{"62516":{"name":"Main Accent","parent":-1}},"gradients":[]},"palettes":[{"name":"Default Palette","value":{"colors":{"62516":{"val":"var(--tcb-skin-color-0)"}},"gradients":[]},"original":{"colors":{"62516":{"val":"rgb(19, 114, 211)","hsl":{"h":210,"s":0.83,"l":0.45}}},"gradients":[]}}]}__CONFIG_colors_palette__
__CONFIG_colors_palette__{"active_palette":0,"config":{"colors":{"49806":{"name":"Main Accent","parent":-1},"3a0f6":{"name":"Accent Light","parent":"49806","lock":{"saturation":1,"lightness":1}}},"gradients":[]},"palettes":[{"name":"Default","value":{"colors":{"49806":{"val":"var(--tcb-skin-color-0)"},"3a0f6":{"val":"rgb(238, 242, 247)","hsl_parent_dependency":{"h":209,"l":0.95,"s":0.36}}},"gradients":[]},"original":{"colors":{"49806":{"val":"rgb(19, 114, 211)","hsl":{"h":210,"s":0.83,"l":0.45,"a":1}},"3a0f6":{"val":"rgb(240, 244, 248)","hsl_parent_dependency":{"h":209,"s":0.36,"l":0.95,"a":1}}},"gradients":[]}}]}__CONFIG_colors_palette__
Previous

Cheap Home Ergonomics: How to Avoid Injuring Yourself

__CONFIG_colors_palette__{"active_palette":0,"config":{"colors":{"49806":{"name":"Main Accent","parent":-1},"3a0f6":{"name":"Accent Light","parent":"49806","lock":{"saturation":1,"lightness":1}}},"gradients":[]},"palettes":[{"name":"Default","value":{"colors":{"49806":{"val":"var(--tcb-skin-color-0)"},"3a0f6":{"val":"rgb(238, 242, 247)","hsl_parent_dependency":{"h":209,"l":0.95,"s":0.36}}},"gradients":[]},"original":{"colors":{"49806":{"val":"rgb(19, 114, 211)","hsl":{"h":210,"s":0.83,"l":0.45,"a":1}},"3a0f6":{"val":"rgb(240, 244, 248)","hsl_parent_dependency":{"h":209,"s":0.36,"l":0.95,"a":1}}},"gradients":[]}}]}__CONFIG_colors_palette__
Next

TDD Kata: How to Level Up Your Test-Driven Swift

Jon Reid

About the author

Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code lead to fear, with real human costs. Looking for ways to make my life better, I learned about Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 19 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Never miss a good story!

Want to make sure you get notified when I release my next article or video? Then sign up here to subscribe to my newsletter. Plus, you’ll get access to the test-oriented code snippets I use every day!

>