# \[Pitch\] Declaring trait configurations for test targets for easier testing

**URL:** <https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371>\
**Category:** Pitches\
**Tags:** packagemanager, testing, traits\
**Created:** [September 4, 2026, 12:29am UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371 "2026-09-04T00:29:19Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![OmChachad](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/omchachad/32/45834_2.png) [@OmChachad](https://forums.swift.org/u/OmChachad)\
**Post date:** [September 4, 2026, 12:29am UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/1 "2026-09-04T00:29:19Z")

</div>

Hello Swift community!

I'd like to pitch an idea that lets you easily test your test targets across different trait configurations in your package. Traits have been a great recent addition to the Swift Package Manager, but when it comes to running unit tests that rely on non-default trait configurations, you'll end up with tests that either fail or cannot compile. This pitch is largely based on proposals from [this forums thread](https://forums.swift.org/t/how-to-test-package-traits/78039). Thanks to @CharlesS and @Cyberbeni for the initial direction and to @FranzBusch and @bripeticca for their inputs on my suggestions!

P.S. This is also my first ever pitch. I'm a part of the Swift Mentorship Program for 2026, and I am very excited to contribute and hear from you! Thanks to @kukushechkin for his guidance throughout the process 🙂

**Original Forums Discussion:** [How to test package traits?](https://forums.swift.org/t/how-to-test-package-traits/78039)  
**Implementation:** [Trait configurations for test targets for easier testing by OmChachad · Pull Request #10480 · swiftlang/swift-package-manager · GitHub](https://github.com/swiftlang/swift-package-manager/pull/10480)  
**Draft Proposal:** [[Draft] SwiftPM: declaring trait configurations for test targets for easier testing by OmChachad · Pull Request #3474 · swiftlang/swift-evolution · GitHub](https://github.com/swiftlang/swift-evolution/pull/3474)

## Motivation

Currently, if a test target tests the output of a function that is gated by a specific non-default trait combination, it will end up failing tests. Consider a package with two traits, where `SomeTrait` is a default trait and `AnotherTrait` is not:

```swift
// swift-tools-version: 6.1
import PackageDescription

let package = Package(
    name: "MyPackage",
    traits: [
        .default(enabledTraits: ["SomeTrait"]),
        "SomeTrait",
        "AnotherTrait",
    ],
    targets: [
        .target(name: "MyLibrary"),
        .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
    ]
)

```

```swift
// Sources/MyLibrary/MyLibrary.swift
public struct MyLibrary {
    public init() {}

    public var isSomeTraitEnabled: Bool {
        #if SomeTrait
        true
        #else
        false
        #endif
    }

    #if AnotherTrait
    /// This API only exists when the `AnotherTrait` trait is enabled.
    public func anotherTraitOnlyAPI() -> String { "enabled" }
    #endif
}

```

A test for the trait-gated API doesn't even compile under the default configuration, and a test for trait-dependent behavior compiles but fails:

```swift
// Tests/MyLibraryTests/MyLibraryTests.swift
@Test func anotherTraitOnlyAPI() {
    // error: value of type 'MyLibrary' has no member 'anotherTraitOnlyAPI'
    // ('AnotherTrait' isn't a default trait, so this doesn't compile under `swift test`)
    #expect(MyLibrary().anotherTraitOnlyAPI() == "enabled")
}

@Test func someTraitDisabled() {
    // Compiles, but fails under `swift test` because 'SomeTrait' is a default trait.
    #expect(!MyLibrary().isSomeTraitEnabled)
}

```

You have to run `swift test` with each trait combination manually to have complete test coverage for the package:

```console
$ swift test # default traits
$ swift test --traits AnotherTrait # AnotherTrait only
$ swift test --traits defaults,AnotherTrait # defaults + AnotherTrait
$ swift test --disable-default-traits # nothing enabled

```

This enumeration usually ends up living in a CI script rather than in the package itself, where it’ll go outdated as traits evolve, and a contributor who’d run a plain `swift test` locally still sees failures for any test target that’s not dependent on a default trait.

You might also try to define multiple tests that are `#if` gated at the top level for a specific trait combination, and then run this with `swift test --enable-all-traits`:

```swift
#if SomeTrait && !AnotherTrait
@Suite struct SomeTraitOnlyTests { /* ... */ }
#endif

#if AnotherTrait && !SomeTrait
@Suite struct AnotherTraitOnlyTests { /* ... */ }
#endif

```

This, however, leads to a lot of duplicate tests and fails to evaluate the case where traits A+B and A+C both do different things when combined, because enabling all traits would have A, B, and C enabled at the same time — the `A+B`-only and `A+C`-only behaviors are never exercised, and both `#if` blocks above simply vanish from the build. The proposed `—-all-trait-combinations` flag in the original traits proposal would solve this, but will cause the other issue of building trait combinations that are never even used, especially for packages with a lot of traits since it would grow exponentially.

Without `#if` gating, even the `swift test --traits` approach will end up in a lot of errors being thrown at your face, since every test target is built under every invocation regardless of which traits it actually needs.

## Proposed solution

The best solution to this problem is to automate the process of running `swift test` for all known trait combinations that you would otherwise perform by hand. Since the package author is likely familiar with all possible valid combinations of traits in their package, defining at the manifest level all the trait configurations that a given test target is valid for, or intends on testing, will allow us to run multiple builds for each valid trait configuration and test the corresponding tests per build. This is done when the `--enable-trait-configurations` flag is passed to `swift test`.

Declaring configurations on the test targets looks like this:

```swift
targets: [
    .target(name: "MyLibrary"),
    .testTarget(
        name: "DefaultTraitsTests",
        dependencies: ["MyLibrary"],
        traitConfigurations: [.default]
    ),
    .testTarget(
        name: "AnotherTraitTests",
        dependencies: ["MyLibrary"],
        traitConfigurations: [
            .enabledTraits(["AnotherTrait"]),
            .enabledTraits(["SomeTrait", "AnotherTrait"]),
        ]
    ),
    .testTarget(
        name: "AllTraitsTests",
        dependencies: ["MyLibrary"],
        traitConfigurations: [.enableAllTraits]
    ),
]

```

A single invocation then runs every declared configuration, building and running only the test targets that declared each one:

```console
$ swift test --enable-trait-configurations
Running tests with default traits
Test Suite 'DefaultTraitsTests' passed
Running tests with traits: AnotherTrait
Test Suite 'AnotherTraitTests' passed
Running tests with traits: AnotherTrait, SomeTrait
Test Suite 'AnotherTraitTests' passed
Running tests with all traits enabled
Test Suite 'AllTraitsTests' failed

Tests failed for 1 of 4 trait configurations: all traits enabled

```

This reduces friction for testing all your traits, reduces the overhead of `#if`-gating test targets, and allows you to focus on the outcome of the tests rather than figuring out how to run them. Because each test target is only built under the configurations it declared, trait-gated tests like `testAnotherTraitOnlyAPI` above simply never get compiled under configurations where they can't compile.

## Detailed design

### Manifest API

A new `traitConfigurations` parameter is added to `.testTarget`, alongside a `Target.TraitConfiguration` type that mirrors the states a trait configuration can take:

```swift
extension Target {
    /// A configuration of the package's traits that a test target declares
    /// support for.
    ///
    /// A test target can declare the trait configurations under which its
    /// tests are expected to run, allowing the package manager to run the
    /// tests once per declared configuration.
    @available(_PackageDescription, introduced: 6.4)
    public struct TraitConfiguration: Hashable, Sendable {
        /// The package's default traits.
        public static let `default`: TraitConfiguration

        /// A configuration with all of the package's traits enabled.
        public static let enableAllTraits: TraitConfiguration

        /// A configuration with all of the package's traits disabled,
        /// including the default traits.
        public static let disableAllTraits: TraitConfiguration

        /// A configuration with the given set of traits enabled.
        ///
        /// The default traits aren't implicitly enabled; include them in the
        /// set to enable them.
        public static func enabledTraits(_ traits: Set<String>) -> TraitConfiguration
    }

    @available(_PackageDescription, introduced: 6.4)
    public static func testTarget(
        name: String,
        dependencies: [Dependency] = [],
        path: String? = nil,
        exclude: [String] = [],
        sources: [String]? = nil,
        resources: [Resource]? = nil,
        packageAccess: Bool = true,
        cSettings: [CSetting]? = nil,
        cxxSettings: [CXXSetting]? = nil,
        swiftSettings: [SwiftSetting]? = nil,
        linkerSettings: [LinkerSetting]? = nil,
        plugins: [PluginUsage]? = nil,
        traitConfigurations: [TraitConfiguration]? = nil
    ) -> Target
}

```

The parameter is validated during manifest loading:

- `traitConfigurations` is only accepted on test targets; declaring it on any other target type is a manifest error.
- An explicitly empty array is a manifest error — a test target must declare at least one configuration when the parameter is specified.
- `.enabledTraits([])` normalizes to `.disableAllTraits`, matching the semantics of passing an empty trait set on the command line today.
- Omitting the parameter (`nil`) means the target declares nothing

Internally, the declared configurations are represented by SwiftPM's existing `TraitConfiguration` model — the same one that backs `--traits`, `--enable-all-traits`, and `--disable-default-traits` so the manifest API introduces no new trait semantics, only a way to spell the existing ones per test target.

### Execution model

Without `--enable-trait-configurations`, `swift test` behaves exactly as it does today, and any `traitConfigurations` declarations are inert.

With the flag, `swift test`:

1. Inspects all test targets of the root packages. A test target that declares no configurations is treated as if it declared `[.default]`, so it runs once under the package's default traits, the same behavior it has today.
2. Collects the unique configurations across all test targets. Two configurations are the same if they are semantically equal (`.enabledTraits(["A", "B"])` and `.enabledTraits(["B", "A"])` are one configuration).
3. Orders the configurations by their first declaration, following manifest declaration order. The run order is therefore deterministic and entirely under the package author's control.
4. For each configuration, in order: resolves and builds the package with that trait configuration, building **only the test products of the targets that declared it** , then runs only those test targets. Each test target builds as its own test product, which is what makes selective building and running possible. Test target is never compiled under a configuration it didn't declare, so trait-gated test code needs no `#if` gating.
5. A failing configuration does not stop the matrix. The remaining configurations still build and run, a summary reports which configurations failed, and the overall invocation exits non-zero if any configuration failed:

```console
Tests failed for 1 of 4 trait configurations: all traits enabled

```

### Build directories

Trait configurations affect the flags of essentially every compile command, so configurations built into a shared build directory would invalidate each other's incremental state on every run. To keep repeated matrix runs incremental, each non-default configuration builds into its own sibling build directory, named by appending a suffix to the regular one:

```swift
.build/
├── out ← default configuration (the regular build directory)
├── out+all-traits ← .enableAllTraits
├── out+no-traits ← .disableAllTraits
├── out+traits-AnotherTrait-SomeTrait ← .enabledTraits(["SomeTrait", "AnotherTrait"]), names sorted
└── checkouts, artifacts, … ← shared caches, unaffected

```

The `.default` configuration deliberately has no suffix: it shares the regular build directory, so the most common configuration stays warm with your ordinary `swift build` and `swift test` invocations. Dependency checkouts, downloaded artifacts, and registry caches are shared across all configurations as they are today. This isolation is scoped to matrix runs; regular invocations with `--traits` continue to use the regular build directory.

In practice this makes a second matrix run over an unchanged package near-instant: in my testing, a two-configuration package went from 27 seconds (first run, both configurations built from scratch) to 4 seconds on the following run.

Additionally, I'd love to hear from you if you work on a package that could directly benefit from this and see how much this change with build directories improves performance depending on the scale of your project.

### Build system support

The feature requires building one test product per test target. The older native build system currently cannot do this, so `--enable-trait-configurations` reports a clear error under `--build-system native`.

## Impact on existing packages

Everything added here is only accessible via the `--enable-trait-configurations` flag. Without this opt-in flag, everything will continue to work as it did before. Manifests that adopt the new `traitConfigurations` parameter require the corresponding tools version; existing manifests are unaffected, and the serialized manifest representation treats the new field as optional, so previously-compiled manifests continue to load. The build directory isolation has also currently been scoped to the flag; however, it can be expanded across SwiftPM if found appropriate.

## Future directions

The original package traits proposal, SE-0450, mentions a `swift test/build/run` flag for package authors called `--all-trait-combinations` in its future directions. @bripeticca, on the same forums thread from earlier, came up with an idea to scope this to a specific set of traits when testing a particular test target, so instead of doing all trait combinations, it would do all combinations of A, B, and C. This has been created as [#10493](https://github.com/swiftlang/swift-package-manager/issues/10493) on swift-package-manager. While this suggestion was for the CLI flag, this can be extended to our `traitConfigurations` parameter.

An `.allCombinationsOf([String])` case could be added to the existing options offered by `TraitConfiguration`. This would only be a part of the manifest-facing type and would act as a generator that expands into plain trait configurations while the manifest is being parsed. This offers a useful tool that can reduce further manual enumeration in some cases, where you would otherwise end up manually listing different combinations of a subset of your package's traits.

## Security

This proposal has no impact on security, safety, or privacy. It adds no new inputs beyond the package's own manifest and runs the same build and test pipeline that `swift test` runs today, once per declared configuration.

## Alternatives considered

Initially, the approach I considered used a `configurations` parameter on the entire `Package` that would let you define all known valid trait combinations for the package; however, I realized it would not provide significant benefits and would be confusing for someone new to traits.

Similar to what @CharlesS's first comment had, I also experimented with an `enabledTraits` parameter which took a set of traits as input. However, realizing that the same test could be worth testing for more than one set of traits, I modified it to accept an array of sets of traits. This largely worked, but I found building off of the existing `TraitConfiguration` model to be the most robust solution yet, because it easily allows expressing the `.default` or `.enableAllTraits` cases without manually enlisting the traits and provides a cleaner approach to this parameter.

Automatically testing every combination of a package's traits, without any declarations was also considered. This doesn't scale: a package with _n_ traits has 2ⁿ combinations, many of which aren't valid or meaningful, and each one is a full build. Declaring the combinations that matter keeps the cost proportional to what the author actually wants covered, which is also why the scoped `.allCombinationsOf` generator above is a future direction rather than the core design.

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [September 4, 2026, 2:27am UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/2 "2026-09-04T02:27:03Z")

</div>

What if I just want it to test all of my trait combinations on the same test target?

---

<div class="post-metadata">

**Author:** ![OmChachad](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/omchachad/32/45834_2.png) [@OmChachad](https://forums.swift.org/u/OmChachad)\
**Post date:** [September 4, 2026, 8:12pm UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/3 "2026-09-04T20:12:35Z")

</div>

Hey! Yes, that could be supported.

SE-0450 mentions an `--all-trait-combinations` flag as a possible future direction, and the linked issue [#10493](https://github.com/swiftlang/swift-package-manager/issues/10493) has that plus a scoped version that generates all combinations of a selected set of traits for the cli.

The same could be exposed via the manifest-facing `TraitConfiguration` API as `.allCombinations` , or `.allCombinations(of: ["A", "B"])` for a selected subset. It'll expand into individual trait configurations for that test target during manifest loading.

The main issue is that it produces 2ⁿ configurations, and some combinations may be invalid or not meaningful. That’s why the current pitch focuses on explicitly declaring only the configurations that matter, with scoped automatic combination generation left as a possible convenience. I suppose `.allCombinations` is mainly useful for those with packages that have only a handful of traits, so it is still a good convenience case to have for those. Might either of cases work for you?

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [September 4, 2026, 9:23pm UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/4 "2026-09-04T21:23:03Z")

</div>

> [@OmChachad](#):
>
> The main issue is that it produces 2ⁿ configurations, and some combinations may be invalid or not meaningful.

While true, it seems like all combinations are part of the package's support requirement, no? And how else would I find invalid combinations? API like `allCombinations(of:)` would be good, as well as just `allCombinations`, but to avoid invalid combinations, something like `allCombinations(except:)` can be used. Or perhaps the package could simply add API to declare invalid trait declarations up front? Or the allowed sets? That way the test level `allCombinations` could avoid them but otherwise find any missed.

---

<div class="post-metadata">

**Author:** ![OneSadCookie](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/onesadcookie/32/3205_2.png) [@OneSadCookie](https://forums.swift.org/u/OneSadCookie)\
**Post date:** [September 4, 2026, 9:51pm UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/5 "2026-09-04T21:51:33Z")

</div>

The default test invocation `swift test` should always do all the necessary testing. The package API change to declare the valid trait combinations for tests (whatever that ends up looking like) is sufficient to gate a new behavior on, we don't need a new CLI flag.

---

<div class="post-metadata">

**Author:** ![OmChachad](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/omchachad/32/45834_2.png) [@OmChachad](https://forums.swift.org/u/OmChachad)\
**Post date:** [September 6, 2026, 4:14am UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/6 "2026-09-06T04:14:32Z")

</div>

> [@Jon\_Shier](#):
>
> While true, it seems like all combinations are part of the package's support requirement, no?

At first I thought so too, but the documentation [explicitly recommends](https://docs.swift.org/latest/documentation/packagemanagerdocs/packagetraits#Define-mutually-exclusive-traits) defining mutually exclusive traits with an `#error` so I guess not, haha. Also, in an example like [swift-log](https://github.com/apple/swift-log), there are 7 traits and if multiple of them are run together, then the most restrictive one takes effect. In this case, running all 128 combinations really just ends up testing 7 combinations. Other packages also implement if-else gating on traits, which means one of them will override the other. Sometimes, the traits can compile and compile and work, but it would be an impractical use case.

Of course, these packages can simply avoid using the `.allCombinations` case and it will work out well, so I'm not opposed to giving the author the choice to opt-in to this behavior if and when found appropriate. I think I will play around with adding this in.

> [@Jon\_Shier](#):
>
> API like `allCombinations(of:)` would be good, as well as just `allCombinations`, but to avoid invalid combinations, something like `allCombinations(except:)` can be used. Or perhaps the package could simply add API to declare invalid trait declarations up front? Or the allowed sets? That way the test level `allCombinations` could avoid them but otherwise find any missed.

I like the idea of the `allCombinations(except:)` variant on this one too. Declaring all valid combinations upfront is an [idea I did float around](https://forums.swift.org/t/how-to-test-package-traits/78039/5) in the original forums post, but the per-test-target configurations approach received far more positive feedback from everyone than the declaring those up front, which is why I ended up going this route. In the other case, it would have been a `traitConfigurations` parameter on the Package itself and you define all the valid combinations, and then reference the ones you'd like to test inside each test target again.

---

<div class="post-metadata">

**Author:** ![OmChachad](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/omchachad/32/45834_2.png) [@OmChachad](https://forums.swift.org/u/OmChachad)\
**Post date:** [September 6, 2026, 4:37am UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/7 "2026-09-06T04:37:40Z")

</div>

> [@OneSadCookie](#):
>
> The default test invocation `swift test` should always do all the necessary testing. The package API change to declare the valid trait combinations for tests (whatever that ends up looking like) is sufficient to gate a new behavior on, we don't need a new CLI flag.

This is a good point. `swift test` should ideally do this by default and this is what @Cyberbeni also suggested originally (and I was in agreement) but I was worried about it being too big of a change for `swift test`'s behavior: it would go from being a single-build command, to one that can perform a lot of builds by default and I didn't want to go against general expectations.

But I think you are right in the fact that declaring the combinations might be good enough reason to gate the multi-build behavior on for `swift test`. My other concern, now, is that this current implementation only works with the swift-build (current default) and not with the native system, so there would be two different behaviors for `swift test` depending on build system.

Curious to hear what others think of this too.

---

<div class="post-metadata">

**Author:** ![orchetect](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/orchetect/32/5098_2.png) [@orchetect](https://forums.swift.org/u/orchetect)\
**Post date:** [September 25, 2026, 6:36pm UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/8 "2026-09-25T18:36:04Z")

</div>

Wholeheartedly support this initiative. I would very much welcome a way to declaratively specify traits for testing at the test target level.

---

<div class="post-metadata">

**Author:** ![OmChachad](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/omchachad/32/45834_2.png) [@OmChachad](https://forums.swift.org/u/OmChachad)\
**Post date:** [October 1, 2026, 7:28pm UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/9 "2026-10-01T19:28:41Z")

</div>

Thank you! Been a little busy but I am going to pick this right up over this weekend. Do you have any feedback based on what was proposed, or things that were brought up in the previous messages?

---

<div class="post-metadata">

**Author:** ![OmChachad](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/omchachad/32/45834_2.png) [@OmChachad](https://forums.swift.org/u/OmChachad)\
**Post date:** [October 1, 2026, 7:32pm UTC](https://forums.swift.org/t/pitch-declaring-trait-configurations-for-test-targets-for-easier-testing/89371/10 "2026-10-01T19:32:44Z")

</div>

Also, I think this got buried in the original post, but if anyone has a package that could benefit from per-test-target traits declaration, I'd love to hear from you, and if you can: please share the link here (or over DMs). I'd love to test these changes out with your package to get a sense for how it interposes with existing packages for whom this might be the most valuable, and do some performance testing! 😄
