Swift Testing - child suites within a suite?

I'm just looking at integrating Swift Testing in to my new project to avoid the weight of XCTest so apologies if this is covered in the documentation but I can't see it.

I'm breaking down a test suite in to subdomains - a common pattern of a Suite relating to a type and subdomains to break down the components of that type to avoid long winded test names with the pattern SUT.functionName.given.when.then() and instead breaking this down in to child suites namespaced for the SUT and the function name.

For example, if my Suite is for a notifications service, I want to break this down to subdomains per function/variable to neatly break up my tests:

struct NotiicationsService {
    var isNotificationsAvailable: Bool { /*logic*/ == true }

    func queueNotifications() { /*queue notifications if needed*/ }
}

Then in my tests I want to break this up per var/func above in to it's own child suite...

@Suite
struct NotificationsServiceTests {
    var sut: NotificationsService
    
    init() {
        self.sut = .production()
    }
    
    @Suite
    struct IsNotificationsAvailable {
        var sut: NotificationsService
        
        init() {
            self.sut = .production()
        }
    }
    
    @Suite
    struct QueueNotifications {
        var sut: NotificationsService
        
        init() {
            self.sut = .production()
        }
    }
}

extension NotificationsServiceTests.IsNotificationsAvailable {
    @Test
    func returnsTrue_whenAuthorisationStatusIsAuthorized()
        #expect()
    }
    
    @Test
    func returnsFalse_whenAuthorisationStatusIsDenied() {
        #expect()
    }
}

extension NotificationsServiceTests.QueueNotifications {
    @Test
    func addsNotificationRequests_whenNewContentIsAvailable() {
        #expect()
    }
    
    @Test
    func addsNotificationRequests_whenUpdatedContentIsAvailable() {
        #expect()
    }
}

This works in my code but the obvious code smell to anyone looking at this is the need to pass down the sut to each "child" suite.

Is there a better way to do this using the existing Swift Testing framework or is this a potential for a new feature - @ChildSuite?

Looking at the current documentation around organising tests:
https://swiftpackageindex.com/apple/swift-testing/main/documentation/testing/organizingtests

... It's implied that child Suites are encouraged:

In addition to containing test functions and any other members that a Swift type might contain, test suite types can also contain additional test suites nested within them. To add a nested test suite type, simply declare an additional type within the scope of the outer test suite type.

But unless I'm missing something (likely) I can't see any more information in these docs around nesting the Suites - especially for my use case of namespacing the different functionality to group tests on a per function basis.

Proposed solution

It would be nice if we could create a macro for @ChildSuite so it inherits the properties and initialiser/deinit by default to clean this up.

This would look like the below:

@Suite
struct NotificationsServiceTests {
    var sut: NotificationsService
    
    init() {
        self.sut = .production()
    }
    
    @ChildSuite
    struct IsNotificationsAvailable {}
    
    @ChildSuite
    struct QueueNotifications {}
}

extension NotificationsServiceTests.IsNotificationsAvailable {
    @Test
    func returnsTrue_whenAuthorisationStatusIsAuthorized()
        #expect()
    }
    
    @Test
    func returnsFalse_whenAuthorisationStatusIsDenied() {
        #expect()
    }
}

extension NotificationsServiceTests.QueueNotifications {
    @Test
    func addsNotificationRequests_whenNewContentIsAvailable() {
        #expect()
    }
    
    @Test
    func addsNotificationRequests_whenUpdatedContentIsAvailable() {
        #expect()
    }
}

I've added a feature request here on the Swift Testing GitHub issues if anyone's interested to adding to the conversation: Feature request: `@ChildSuite` for breaking down Suites in to namespaced subdomains · Issue #296 · apple/swift-testing · GitHub

Unfortunately macros cannot accomplish what you want here. They are, by design, unable to see members of containing types.

Even if they could do it, there's a huge amount of nuance involved. What do you do if two parent types both have a name field? What do you do if one parent type is actor-isolated and another is actor-isolated to a different actor? What do you do if a type is embedded in an extension to another type, in which case the original declaration is unavailable? These questions do not have trivial or obvious answers.

It seems like you're sort of reinventing polymorphism here. We don't currently support non-final classes as suites, but that's a technical constraint we hope to eventually lift, and subclassing would probably solve the problem.

Failing that, consider grouping tests into extensions on the same type rather than placing them in different types entirely. Or, place common members in a helper type and include a property of that type on your suite types.

1 Like