Issue with State Persistence in Swift Testing @Suite

Hello everyone,

I’m encountering an issue with my Swift Testing suite where state changes made in one test method do not persist to another. I am using Swift’s @Suite and @Test annotations to group and serialize my tests, but it seems that the state is not being carried over between the tests. The second function fails with: Expectation failed: (string → nil) != nil

Here is my example code:

import Testing

@Suite(.serialized)
struct createCheckTests {
    var value = 25
    var string: String? = nil
    
    @Test("Create string")
    mutating func stringCreation() {
        #expect(value > 0)
        string = "Value is: \(value)"
    }
    
    @Test("Check string")
    func stringCheck() {
        #expect(string != nil, "The string is nil")
        print("\(String(describing: string))")
    }
}

What is the correct way to approach such a scenario where I want to test two functions that are related, one to generate some value and one to check that generated value against it initial value using Suites to group and isolate them from other tests?

Thanks.

Answered by Developer Tools Engineer in 792338022

Hi @TommyGun! Each test function gets its own instance of its suite type (if it's contained in one.) So stringCreation() and stringCheck() will operate on separate instances of createCheckTests. The presence of the .serialized trait does not affect this functionality.

Having one test function directly rely on the output of another is not something that Swift Testing supports, nor is it something we'd generally recommend as it's very difficult to correctly model inter-test dependencies. Instead, combine the two tests into a single test. For example:

@Suite(.serialized)
struct createCheckTests {
    var value = 25
    var string: String? = nil
    
    @Test("Create and check string")
    mutating func stringCreationAndChecking() {
        #expect(value > 0)
        string = "Value is: \(value)"

        #expect(string != nil, "The string is nil")
        print("\(String(describing: string))")
    }
}

In the case where multiple tests have similar or identical setup, you can move the setup code up to init(). If you do that, then your test code can be simplified because you no longer need string to be optional (in this example):

@Suite(.serialized)
struct createCheckTests {
    var value: Int
    var string: String

    init() {
        value = 25
        string = "Value is: \(value)"
    }
    
    @Test("Check string")
    func stringCheck() {
        #expect(value > 0)
        print(string)
    }
}

Now, obviously real-world tests will be more complex than this trivial one, but hopefully I've made it clear how you can leverage init() to abstract away your creation logic.

With that said, if you really really need shared state, you can use a nonisolated(unsafe) static var on your suite type instead of an instance property, but keep in mind it's not a recommended pattern and is unsafe to use with parallelized tests. We talk about some of these anti-patterns in the last section of Go further with Swift Testing.

Accepted Answer

Hi @TommyGun! Each test function gets its own instance of its suite type (if it's contained in one.) So stringCreation() and stringCheck() will operate on separate instances of createCheckTests. The presence of the .serialized trait does not affect this functionality.

Having one test function directly rely on the output of another is not something that Swift Testing supports, nor is it something we'd generally recommend as it's very difficult to correctly model inter-test dependencies. Instead, combine the two tests into a single test. For example:

@Suite(.serialized)
struct createCheckTests {
    var value = 25
    var string: String? = nil
    
    @Test("Create and check string")
    mutating func stringCreationAndChecking() {
        #expect(value > 0)
        string = "Value is: \(value)"

        #expect(string != nil, "The string is nil")
        print("\(String(describing: string))")
    }
}

In the case where multiple tests have similar or identical setup, you can move the setup code up to init(). If you do that, then your test code can be simplified because you no longer need string to be optional (in this example):

@Suite(.serialized)
struct createCheckTests {
    var value: Int
    var string: String

    init() {
        value = 25
        string = "Value is: \(value)"
    }
    
    @Test("Check string")
    func stringCheck() {
        #expect(value > 0)
        print(string)
    }
}

Now, obviously real-world tests will be more complex than this trivial one, but hopefully I've made it clear how you can leverage init() to abstract away your creation logic.

With that said, if you really really need shared state, you can use a nonisolated(unsafe) static var on your suite type instead of an instance property, but keep in mind it's not a recommended pattern and is unsafe to use with parallelized tests. We talk about some of these anti-patterns in the last section of Go further with Swift Testing.

Thank you for the reply. It's exactly Go further with Swift Testing that I watched when I came across the .serialized option. I wrongly assumed it was for use cases like this where one was forced to run after the other in order to share state. Ok I will rewrite the @Test in this case to get used to Swift Testing way of dealing with instances of Suite types.

Thanks

Issue with State Persistence in Swift Testing @Suite
 
 
Q