Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

Swift Testing: Validate your code using expressive APIs

Apple announced the Swift Testing framework during WWDC 2024. It transforms the way we write tests in Swift. A new clear, expressive API makes writing tests more straightforward, while the Xcode user interface communicates with improved feedback when a test fails or succeeds.

The testing framework embraces Swift Macros, reducing the boilerplate code you must write for repetitive tests. So-called parameterized tests help you run similar tests over a sequence of values. Hence, you no longer have to duplicate and maintain an almost identical test for a different input value. Let’s dive into how Swift Testing works and how you can use it today.

Note: this article covers an introduction to Swift Testing, and more in-depth articles will be referenced accordingly over the upcoming weeks. If you don’t want to miss any, subscribe to my weekly newsletter.

Writing tests using Swift Testing

If you’re new to testing, there’s not much to compare. However, if you’re used to XCTests, quite a few things are changing when writing tests using the Swift Testing framework.

First of all, we’ll have to import a different framework:

import Testing

After this import, we can start defining our first tests. What’s interesting here is that we can define tests globally:

import Testing
@testable import SwiftTestingPlayground

@Test func personFullName() {
    let person = Person(firstName: "Antoine", lastName: "van der Lee")
    #expect(person.fullName == "Antoine van der Lee")
}

Note that we’re importing SwiftTestingPlayground using the @testable attribute to allow access to internal types like the Person struct.

We’ve defined the test using a new @Test macro, which replaces the XCTest test method prefix. Inside the test, we’re making use of the #expect macro, which replaces assertions like XCAssert. Make sure to forget the old habit of writing test as a prefix. I actually started writing this article with a test method named testPersonFullName , which is no longer needed due to the @Test macro.

After running the test for the first time, we can look into the test navigator and evaluate the hierarchy:

The Test Navigator in Xcode shows the tests we’ve written using Swift Testing.

While the global test works fine in a small project like this, you’re likely looking for a better way to organize your tests.

How do you stay current as a Swift developer?

Let me do the hard work and join 19,138 developers that stay up to date using my weekly newsletter:

Organizing tests in Swift Testing

The Swift Testing framework allows you to organize tests using metadata like traits and tags or wrapping tests using structs. We’ll dive deeper into traits and tags in a future article (which I’ll reference here), so let’s see how a parent struct affects the organization of tests.

In this case, we’re testing person-specific code. Therefore, it makes sense to call our wrapper PersonTests:

import Testing
@testable import SwiftTestingPlayground

struct PersonTests {
    @Test func fullName() {
        let person = Person(firstName: "Antoine", lastName: "van der Lee")
        #expect(person.fullName == "Antoine van der Lee")
    }
}

We can rename the test method to no longer contain “person” since the outer struct makes this clear enough.

Assume you’re going to write more tests soon, you might want to go one step further and add another layer called Names to focus on name-related tests only:

import Testing
@testable import SwiftTestingPlayground

struct PersonTests {
    
    @Test func initialization() {
        let person = Person(firstName: "Antoine", lastName: "van der Lee")
        #expect(person.firstName == "Antoine")
        #expect(person.lastName == "van der Lee")
    }
    
    struct Names {
        @Test func fullName() {
            let person = Person(firstName: "Antoine", lastName: "van der Lee")
            #expect(person.fullName == "Antoine van der Lee")
        }
    }
}

The updated hierarchy inside the test navigator looks as follows:

You can group tests using structs as wrappers.
You can group tests using structs as wrappers.

These are just the basics of what you can do to organize your tests. Stay tuned for more in-depth articles in which we’ll cover tags and traits.

Taking a closer look at the #expect macro

Swift Testing is driven by macros from which the #expect macro has most of the magic. While we had to use all kinds of assertion methods in XCTest, we can now focus on a single method that works magically with anything you use as input.

A few examples of how the #expect macro transforms into clear failure messages.
A few examples of how the #expect macro transforms into clear failure messages.

There are many ways to define conditional statements, and the macro is smart enough to transform them into specific failure outputs. This becomes even better when you decide to show more details inside Xcode:

The expect macro works closely together with Xcode to provide detailed failure information.
The #expect macro works closely together with Xcode to provide detailed failure information.

These details will help you solve tests more quickly by making determining what caused the failure easier. In this case, we can see that the person’s full name is defined as “Antoine van der Lee” while our input String matches “Antoine Lee”.

Migrating existing XCTests to Swift Testing

If you’ve written tests before, you’re likely interested in how to migrate them to Swift Testing. Apple also knew about this requirement and decided to write an in-depth migration article.

Conclusion

The Swift Testing framework transforms how we write tests in Swift and prepares us for the future. We’ve only seen the basics today, but I’ll update this article in the upcoming weeks with references to more in-depth articles on each macro, test traits, test organization, and more.

Here are a few more articles for you to make the most out of Swift Testing:

Thanks!

 
Antoine van der Lee

Written by

Antoine van der Lee

iOS Developer since 2010, former Staff iOS Engineer at WeTransfer and currently full-time Indie Developer & Founder at SwiftLee. Writing a new blog post every week related to Swift, iOS and Xcode. Regular speaker and workshop host.

Are you ready to

Turn your side projects into independence?

Learn my proven steps to transform your passion into profit.