Are trait stored properties shared across all tests when attached at the @Suite level?

Hi all :waving_hand:,

I'm exploring the new Swift Testing framework, and came across a behavior I'd like to confirm.

Let’s say I define a custom trait like this:

struct MyTrait: TestTrait, TestScoping {
    let value: UUID = UUID()

    func provideScope(
        for test: Test,
        testCase: Test.Case?,
        performing function: @Sendable () async throws -> Void
    ) async throws {
        print("Trait value:", value)
        try await function()
    }
}

And apply it like this:

@Suite("MySuite", MyTrait())
struct MyTests {
    @Test func test1() { }
    @Test func test2() { }
}

When I run the tests, both test1 and test2 print the same UUID, meaning the same MyTrait instance is reused — even though provideScope() is called separately for each test.

However, if I attach MyTrait() individually to each test, the UUID is different — which suggests a fresh trait instance is used per test.

:red_question_mark: My Question:

Is it expected behavior that:

  • Traits applied at the @Suite level are created once and shared across all tests in the suite?
  • Even though provideScope() is called per test, all tests access the same stored properties?

If so, would the best practice be to:

  • Avoid storing per-test data in suite-level trait properties?
  • Use helper methods or closures to generate fresh values inside provideScope()?

Thanks for the clarification!

Hey @pavm035,

A couple things to note first:

  • As written, this code doesn't compile because MyTrait doesn't conform to SuiteTrait. Only trait types which conform to SuiteTrait may be applied to suites via the @Suite(...) attribute. I'll assume in your "real" code you did have this conformance, though.
  • A trait applied to a @Suite(...) is only inherited by the @Test functions within that suite if it overrides the SuiteTrait.isRecursive property to true. Otherwise, the trait only applies to the suite itself, and, if the trait also conforms to TestScoping, the provideScope() method is only called once, for the suite itself, rather than being called once for each of the suite's test functions.

Given that, I'll try to answer your questions:

It depends on the isRecursive property, as I mentioned above. But even if isRecursive is true, a copy of the trait applied to the suite is what will be inherited by its test functions. When a trait from a suite is inherited by a test function, it does not re-evaluate the expression which was originally used to create the trait instance (MyTrait()), so in this example the MyTrait value gets copied to the two @Test functions, meaning the let value: UUID properties of all trait instances would be the same. (Again, this is all assuming isRecursive is true.)

provideScope() will be called on the instance of the trait applied to, or inherited by, the relevant test. One way to think about this scenario is that there are 3 total copies of MyTrait, and they each have the same value for the let value: UUID property.

I would generally agree with this. Although note that in your example, MyTrait is a struct, and value semantics will generally prevent you from sharing data in the way you might be thinking.

I'm not certain what kind of fresh data you mean. Perhaps it would help to explain what your overall goal is.

Hope that helps!
Stuart

1 Like

Hi @smontgomery,

Thanks for the quick and detailed feedback — very helpful!

As written, this code doesn't compile because MyTrait doesn't conform to SuiteTrait. Only trait types which conform to SuiteTrait may be applied to suites via the @Suite(...) attribute. I'll assume in your "real" code you did have this conformance, though.

Yes, you're absolutely right — I forgot to include the SuiteTrait conformance in the example, but I do have it in the actual code.

I would generally agree with this. Although note that in your example, MyTrait is a struct, and value semantics will generally prevent you from sharing data in the way you might be thinking.

Understood. But as you mentioned, tests inside the suite inherit a copy of the MyTrait instance from the suite. So if we were to include a mutable property in MyTrait (which I didn’t show in the example), it could lead to data races or unexpected shared state — especially in concurrent scenarios.

I'm not certain what kind of fresh data you mean. Perhaps it would help to explain what your overall goal is.

Sorry for not being clear — by "fresh data", I meant that instead of storing test dependencies as properties inside MyTrait, I can pass in closures that generate new instances per test.

For example:

struct MyTrait: TestTrait, SuiteTrait, TestScoping {
    let makeMockAPI: @Sendable () -> MockAPI

    func provideScope(...) async throws {
        let api = makeMockAPI() // ← fresh instance for each test
        ...
    }

    var isRecursive: Bool = true
}

This approach ensures isolation and avoids shared state, while still allowing common setup at the suite level.

My overall goal is to simplify test setup by providing common mock dependencies (e.g., mock services, configurations) at the suite or sub-suite level using a shared trait. At the same time, I’d like to allow individual tests to override those dependencies when needed by attaching their own customized version of MyTrait. This mirrors the pattern we’ve used in Quick/Nimble with beforeSuite, beforeEach, and so on, and we’re now looking to migrate those test cases to Swift Testing.

If there are any recommended best practices or alternative approaches for handling this pattern — especially around trait composition, overriding, or scoped setup — I’d really appreciate any guidance.

Thanks again for the clarification — this really helped solidify my understanding of how traits are constructed and propagated!