Meet Swift Testing

Joel Oliveira
Joel Oliveira
Jul 12 2024
Posted in Engineering & Technology

One more tool for iOS developers

Meet Swift Testing

In this post, we will quickly cover one of the developer tools announced during WWDC 2024. We are excited to take Swift Testing for a spin.

Ensuring quality and reliability is essential for delivering an exceptional user experience. Automated testing has consistently proven to be the key to achieving and sustaining software excellence. This innovative suite of tools is designed to make testing Swift code simpler and more robust than ever before.

Swift Testing is crafted specifically for Swift, harnessing cutting-edge features such as concurrency and macros. It supports all major platforms, including Linux and Windows. And with an open development process, the entire community is invited to actively participate in shaping its future.

If you're currently using XCTest and are curious about how it compares to Swift Testing or how to migrate, we recommend checking out this guide. While there are similarities between the two, there are also significant differences. Both frameworks can coexist within the same target, allowing you to gradually adopt Swift Testing.

Swift Testing offers a powerful set of features that enhance the organization, classification, and execution of tests. However, you should continue using XCTest for tests that utilize UI automation APIs like XCUIApplication, performance testing APIs like XCTMetric, or for tests that need to be written in Objective-C, as these are not yet supported in Swift Testing.

Getting Started with Swift Testing

If you’ve never written tests for your app before, the first step is to add a test bundle target to your project. In Xcode, choose File > New > Target. You can then search for Unit Testing Bundle:

In Xcode 16, Swift Testing is now the default choice of testing system for this template.

Core Concepts

Let's explore the building blocks of Swift Testing, and how they offer a more modern approach to testing than what XCTest has to offer.

Testing Functions

Adding the @Test attribute to a function designates it as a test. Once this is done, Xcode recognizes it and displays a Run button alongside it.

import Testing

@Test func productComments() {
    // ...
}

Test functions are simply regular Swift functions with the @Test attribute. They can be global functions or methods within a type, and they have the flexibility to be marked as async, throws, or isolated to a global actor if necessary.

You can also wrap multiple related tests in a struct to automatically group multiple test (aka @Suite):

struct ProductCommentsTests {

    @Test func productComments() {
    // ...
    }

    @Test func productCommentRatings() {
    // ...
    }

}

Expectations

These are the macros you will use to test your code, by validating that an expected condition is true. For example, the #expect macro performs an expectation, and it accepts ordinary expressions and language operators, as follows:

@Test func productSku() {
    let product = Product(productId: "123456789")
    #expect(product.sku === "ABC123")
}

The #expect macro is really flexible. You can pass any expression, including operators or method calls, and it will show you detailed results if it fails.

#expect(1 == 2)
#expect(comment.user.name == "Joel")
#expect(!comments.isEmpty()))
#expect(arrayOfNumbers.contains(2)))

But sometimes you simply want to end the test if a certain expectation fails. For this, you can use the #required macro:

@Test func sessionInvalidate() {
    try #required(session.isAlive)
    session.invalidate();
}

Or you can use it to safely unwrap optional values, as follows:

let user = try #required(users.first)
#expect(user.isActive)

Traits

Traits can be used to customize your tests and suites, organize them or define when and if they should run. For example, you can add a custom display name to your test, by using the following:

@Test("Test Product Comments") func productComments() {
    // ...
}

You can also use tags to better organize tests that relate to the same feature, as follows:

@Test(.tags(.informational)) func productComments() {
    // ...
}

Xcode will then group these test together and allow you to filter tests by tags in the Test Navigator.

You can also use traits to define if the test should run based on certain conditions. For example, you can define if a test should run only if a certain feature is available:

@Test(.enabled(if: AppFeatures.isCommentingEnabled))
func commentProduct() {
    // ...
}

Or if you want to selectively disable a certain test based on a known issue:

@Test(.disabled("Known Issue"), .bug("example.org/bugs/xxx", "Crashes with..."))
func commentProduct() {
    // ...
}

Or if you want to make sure it only runs on a certain OS version:

@Test("Test New Feature")
@available(macOS 15, *)
func newFeature() {
    // ...
}

And you can also define the behavior of your tests when they run, using the following:

@Suite(.serialized)
struct ProductCommentsTests {

    @Test func productComments() {
    // ...
    }

    @Test func productCommentRatings() {
    // ...
    }
}

Suites

Suites are used to group related test functions or other suites. They can be annotated explicitly using the @Suite attribute. By default, any type which contains @Test functions or @Suites is considered a @Suite itself.

Suites can have stored instance properties, and they can use init or deinit to perform logic before or after each test.

Let's take a look at this example:

@Suite("Test Product Component")
struct Product {

    @Test func productThumbnail() {
        let product = Product(prodId: "123456")
        let expectedThumbnail = ProductThumbnail(url: "https://images.cdn.com/xxx")
        #expect(product.thumbnail == expectedThumbnail)
    }

    @Test func productCategory() {
        let product = Product(prodId: "123456")
        #expect(product.category == "Shoes")
    }

}

These tests start the same way, they initialize the same product component. Since they are in suite, you can reduce repetition of code by refactoring it as follows:

@Suite("Test Product Component")
struct Product {

    let product = Product(prodId: "123456")

    @Test func productThumbnail() {
        let expectedThumbnail = ProductThumbnail(url: "https://images.cdn.com/xxx")
        #expect(product.thumbnail == expectedThumbnail)
    }

    @Test func productCategory() {
        #expect(product.category == "Shoes")
    }

}

Cool, right?

This is just a small introduction to Swift Testing, and we strongly encourage you to read more about it in the official Apple's documentation. According to Apple, this is just the beginning for this new package.

We are also pleased to see Apple's effort in supporting other platforms and the direction of Swift as an open source project.

As always, you can find us available for any question you might have via our Support Channel.

Keep up-to-date with the latest news