.st0{fill:#FFFFFF;}

How to Unit Test Enumerations in Swift 

 November 10, 2020

by  Jon Reid

You have an enumeration. How can you write an XCTest assertion to verify it? What about associated values? And how do we avoid overspecified tests?

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 ways to test enums. A separate post shows how to test optionals.

Test the Value of a Simple Swift Enumeration

Swift enumerations without associated values are simple to test. For example:

enum PlainAnimal {
    case cat
    case dog
}

Let’s test a computed property that returns a PlainAnimal. In this case, the value is hard-coded.

var simpleAnimal: PlainAnimal { .cat }

To test the result, use XCTAssertEqual:

func test_simpleEnum_withoutAssociatedValues() throws {
    let result = simpleAnimal
    XCTAssertEqual(result, .dog)
}

This fails with the following message, which looks good:

XCTAssertEqual failed: ("cat") is not equal to ("dog")

Test the Value of an Objective-C Enumeration

Though we code in Swift, Apple’s frameworks are written in Objective-C. Swift interoperability makes it easy to use Objective-C enumerations from Foundation and UIKit. But what about in unit tests?

Here’s a computed property that returns a UIButton. It doesn’t need any special setup for our example, so it just creates a new button with default properties.

var button: UIButton { UIButton() }

Let’s write a test that checks the button type.

func test_oldEnum() throws {
    let result = button
    XCTAssertEqual(result.buttonType, .close)
}

I chose a buttonType different from the default value of .custom so that we can check the test failure message. Here’s what we get:

XCTAssertEqual failed: ("UIButtonType") is not equal to ("UIButtonType")

Well, that’s not helpful. The test will pass with a matching value. And here, it does fail with a mismatch. But it doesn’t tell us what the values were!

One way to improve this is to compare their raw values instead:

func test_oldEnum_usingRawValue() throws {
    let result = button
    XCTAssertEqual(
        result.buttonType.rawValue,
        UIButton.ButtonType.close.rawValue
    )
}

Here’s the failure message we get now:

XCTAssertEqual failed: ("0") is not equal to ("7")

That’s definitely an improvement. At least now we can see what the actual value was, and how it differed from the expected value. But we’d have to go over to the ButtonType definition to see which one is 0 to understand the issue. And it forces us to write clumsy test code.

We can do better. As I describe in the “Testing Text Fields” chapter of iOS Unit Testing by Example, Swift doesn’t know how to describe Objective-C enumeration values. We can help it by adding extensions to make such types conform to the CustomStringConvertible protocol.

extension UIButton.ButtonType: CustomStringConvertible {
    public var description: String {
        switch self {
        case .custom:
            return "custom"
        case .system:
            return "system"
        case .detailDisclosure:
            return "detailDisclosure"
        case .infoLight:
            return "infoLight"
        case .infoDark:
            return "infoDark"
        case .contactAdd:
            return "contactAdd"
        case .close:
            return "close"
        @unknown default:
            fatalError("Unknown UIButton.ButtonType")
        }
    }
}

You don’t have to do this for every Objective-C enumeration—only the ones you test. Define each extension on an as-needed basis.

Once we provide this extension, the test now fails with the following message:

XCTAssertEqual failed: ("custom") is not equal to ("close")

This failure message has the clarity we want.

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.

Test All Values of an Enum Case with Associated Values

Let’s get to the powerhouse that Swift adds to enumerations: cases with associated values. Here’s the example we’ll use:

enum Authentication {
    case basic(userID: String, password: String)
    case knockOnDoor(times: Int)
}

Here’s a computed property which returns an Authentication:

var authentication: Authentication {
    .basic(userID: "user", password: "hunter2")
}

What if you want to test for an entire case, along with all the values that case brings? That is, you want to compare for .basic with two specific strings, or .knockOnDoor with a specific integer?

As long as every type within an enumeration is Equatable, we can declare the enumeration to also be Equatable:

enum Authentication: Equatable {

Once the enumeration is Equatable, we can use XCTAssertEqual.

func test_associatedValues_fullEquality() throws {
    let result = authentication
    XCTAssertEqual(
        result,
        .basic(userID: "user", password: "secret")
    )
}

Remember, we want to check how failure messages look, so always make new tests fail. Here’s the failure message for this test:

XCTAssertEqual failed: ("basic(userID: "user", password: "hunter2")") is not equal to ("basic(userID: "user", password: "secret")")

XCTAssertEqual makes it easy to write tests that compare all associated values. But the disadvantage lies in the failure message. Authentication.basic only has two associated properties, but it’s already hard to spot the mismatch.

Test Only One Associated Value of an Enum Case

Making your enumerations Equatable and using XCTAssertEqual looks simple, and can be the right thing to do for simple values. But it adds complications when:

  • An enumeration case has several associated values.
  • Or, an associated value is a struct with many properties.

Either way, it’s like a tree with many leaves. Using XCTAssertEqual complicates failure messages. It can also lead to overspecified tests.

We want tests that are sensitive to the things we care about, but insensitive to details that don’t matter. An overspecified test is sensitive to irrelevant details. This results in fragile tests that fail too easily.

An overspecified test is sensitive to irrelevant details. This results in Fragile Tests that fail too easily.

Click to Tweet

We can think of this around test input and test output. For input, we often need to provide the System Under Test (SUT) with a value or object that isn’t relevant to the test. xUnit Test Patterns calls this a Dummy Object.

A Dummy Object is a placeholder object that is passed to the SUT as an argument (or an attribute of an argument) but is never actually used.

“Dummy Object”  is one of the test patterns 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.)


For example, if we’re passing in an enumeration case with associated values, we can’t leave any values out. Or if we’re passing in a struct, we can’t leave any properties out. We have to fill every slot with something. Any values that don’t matter to the test are dummies.

But what about the output? An enum will come with its values, and a struct or class will come with its properties. To avoid fragile tests, how can we ignore some values, and assert on only the values we care about?

Let’s look at two examples. In one, the enumeration case has two associated values, but we’ll examine only of them. In another, the case will come with a struct, but we’ll only look at a single property of that struct.

Testing a Single Associated Value

To extract a single value from an enumeration case, we can make the test code smarter. It needs to handle multiple points of failure:

  • If it receives the wrong case, the test should report it and stop.
  • Then we can assert what we like on the associated value we care about.

Let’s build up our test code to fail at either point. Here’s a test that fails because it receives the wrong case:

func test_associatedValues_exampleFailingOnWrongCase() throws {
    let result = authentication
    guard case let .knockOnDoor(times: times) = result else {
        XCTFail("Expected knockOnDoor, but was \(result)")
        return
    }
    XCTAssertEqual(times, 3, "times")
}

Note how the XCTFail message precisely states the mismatched case:

failed - Expected knockOnDoor, but was basic(userID: "user", password: "secret")

Here’s the pattern I follow: Avoid switch statements in test code, because that leads to code which intermingles the success and failure cases. Instead, use guard case let with an XCTFail and a return. If it fails, the return halts the test. The success case flows through, and this keeps the test code in top-down order. Because the test jumps through multiple hoops, keeping those hoops in order makes it easier to read.

Let’s write another test that makes it through the guard case let, but fails afterward.

func test_associatedValues_extractOneValueForTesting() throws {
    let result = authentication
    guard case let .basic(userID: userID, password: _) = result else {
        XCTFail("Expected basic, but was \(result)")
        return
    }
    XCTAssertEqual(userID, "zzz")
}

Here’s the resulting failure message:

XCTAssertEqual failed: ("user") is not equal to ("zzz")

You may wonder why I didn’t inline the expected userID into the guard case, like this:

guard case .basic(userID: "user", password: _) = result else {

This certainly works—the guard will fail if it doesn’t match both the case and any specified values. And there may be times when this style is more expressive. (If you use this style, don’t forget to include all expectations in the XCTFail message.)

But I prefer to use guard case let to extract the associated value, then test it separately. It’s become a pattern I can use for every enum with associated values, regardless of the complexity of the associated value itself. It still applies in the following section.

Testing a Single Property of an Associated Struct

Sometimes an associated value is something with several properties, like a class or a struct. For example, let’s borrow a struct from the other post showing how to unit test optionals.

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

Now let’s add a new Authentication case to represent an introduction from a Person.

enum Authentication: Equatable {
    case basic(userID: String, password: String)
    case knockOnDoor(times: Int)
    case introduction(from: Person)
}

How can we write a test for an introduction, where we only care about the person’s id property, and not whether they have a middle name or are a zombie? We can do this by following the same pattern above—use guard case let to extract the person. Then test that person’s id.

func test_associatedValues_extractPersonForTesting() throws {
    let result = authentication
    guard case let .introduction(from: person) = result else {
        XCTFail("Expected introduction, but was \(result)")
        return
    }
    XCTAssertEqual(person.id, "KEY")
}

Finally, if we only care about the case but not about its associated values, use guard case instead of guard case let. Leave the values out.

func test_associatedValues_checkIntroductionOnly() throws {
    let result = authentication
    guard case .introduction = result else {
        XCTFail("Expected introduction, but was \(result)")
        return
    }
}

Which You Use Depends on Which You Have

Let’s bring it all home.

If you have a Swift enumeration with no associated values, define it as Equatable, and use XCTAssertEqual.

If you’re testing an enumeration from Apple’s frameworks, use XCTAssertEqual but check the resulting failure message. It’s likely to be an Objective-C enumeration which Swift won’t know how to describe. Add an extension to make it conform to CustomStringConvertible.

If you want to test for an enumeration case that has associated values, consider which values (and sub-values) are meaningful for your test.

  • If you want to compare every value, define your enum as Equatable and use XCTAssertEqual.
  • If you don’t care about any associated values, use guard case. Put an XCTFail and return combination in the else clause.
  • If you want to extract a particular associated value, use guard case let. Put an XCTFail and return combination in the else clause. In the fall-through below the guard statement, assert what you want about the extracted value.

If you have any questions or remarks, leave a comment below. Happy testing!

Power Up Your Unit Testing

Download our free test-oriented code snippets to improve the “flow” of writing unit tests.

__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

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"}
>