Library / SDK
swiftlang/swift-testing avatar
swiftlang/swift-testing

swift-testing: the test framework that already ships inside your Swift toolchain

A modern, expressive testing package for Swift

2,174 stars169 forksSwiftApache-2.0

At a glance

What is it?
Swift Testing is a macro driven test framework bundled with the Swift 6 toolchain and Xcode 16, where failed expectations report the values involved rather than just a line number.
Who is it for?
Swift Testing is the framework that arrived with Swift 6 and Xcode 16, and the strongest reason to adopt it is that there is nothing to install. A failed #expect prints the expression with the values it actually compared, which shortens the loop between running a suite and understanding what broke.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A failed expectation prints the values it compared

The smallest example in the repository already shows the point of the whole design. A failed expectation is reported with the expanded expression and the real value, not with a line number that leaves you to guess:

swift
import Testing

@Test func helloWorld() {
  let greeting = "Hello, world!"
  #expect(greeting == "Hello") // Expectation failed: (greeting → "Hello, world!") == "Hello"
}

That trailing comment is the actual diagnostic. XCTest assertions take closures or bare values, so when one fails you often get `XCTAssertEqual failed` and nothing about what was on either side. Swift Testing builds its assertions on macros, which means the framework sees the source expression before it is evaluated and can keep both the operation and its result for the failure message. When a suite goes red, the output names the variable and the value.

The test declaration itself is a plain function, decorated. There is no class to subclass and no base test case to inherit from, and a test can be `async`, `throws`, or both without any ceremony. That is a smaller surface than the XCTest arrangement, and it is also why migration is described as incremental rather than total.

Traits carry runtime conditions, tags carry selection

Two mechanisms handle the metadata most test suites end up bolting on with naming conventions. Traits attach to a test or suite and describe when it should run. Tags attach the same way and describe what the test is about, so you can select a slice of a suite later without reorganising files.

A trait can gate on a runtime condition, which is the useful case for feature flags. The example in the README shows a commenting test that only runs when the feature is on:

swift
@Test(.enabled(if: AppFeatures.isCommentingEnabled))
func videoCommenting() async throws {
    let video = try #require(await videoLibrary.video(named: "A Beach"))
    #expect(video.comments.contains("So picturesque!"))
}

A tag does the naming and the grouping instead, and pairs with a display name so the test reads well in output:

swift
@Test("Check video metadata",
      .tags(.metadata))
func videoMetadata() {
    let video = Video(fileName: "By the Lake.mov")
    let expectedMetadata = Metadata(duration: .seconds(90))
    #expect(video.metadata == expectedMetadata)
}

The README also states that traits can set execution time limits, which is the piece aimed at continuous integration: a suite can declare its own budget instead of having a global timeout configured somewhere else. Note that `#require` is the companion to `#expect`. It unwraps an optional or throws when the value is missing, so a missing fixture stops the test rather than cascading into a confusing failure further down.

Arguments table drive, and everything runs in parallel

Parameterized tests are declared with an `arguments:` list, and the same test body runs once per element:

swift
@Test("Continents mentioned in videos", arguments: [
    "A Beach",
    "By the Lake",
    "Camping in the Woods"
])
func mentionedContinents(videoName: String) async throws {
    let videoLibrary = try await VideoLibrary()
    let video = try #require(await videoLibrary.video(named: videoName))
    #expect(video.mentionedContinents.count <= 3)
}

Each element becomes its own case, so a failure names the input. The practical effect on a large suite is less repetition in the test body and a longer failure report, which is usually the trade you want.

The other default is worth stating plainly: tests run in parallel, because the framework integrates with Swift Concurrency rather than wrapping it. Combined with 2171 stars, 168 forks and 131 open issues on the repository, that gives a fair picture of a young framework under real load. Parallel execution means a suite that touches shared mutable state, a fixed port, or a single temporary directory can fail intermittently without any code change, and the fix belongs in the test design rather than in the framework. If you are porting an existing suite, look for shared fixtures before you switch it over, because the old serial order may have been hiding the coupling for you.

Build only in the support table means the tests never ran

The platform table in the README has three columns: platform, support status, and qualification. Apple platforms, Linux, FreeBSD and Windows are all Supported with Automated qualification. Wasm and Android are Experimental with Automated qualification marked Build Only. OpenBSD is Experimental with Manual qualification, and that cell links to a workflow file named `manual_build_openbsd.yml`.

The footnote under the table explains that Automated means continuous integration verifies the project builds and passes all tests, while Build Only means the platform relies on manual test, and Manual means manual build and manual test. Read carefully, that leaves a gap. Two platforms carry the word Automated in the same column as four platforms that genuinely run their tests, but the parenthetical is the only thing separating them. A contributor reading the table alone can reasonably conclude that WebAssembly changes are checked by running WebAssembly tests, when in practice the build is verified and the suite is not executed there.

The honest reading is narrower than the table looks. Apple, Linux, FreeBSD and Windows have continuous coverage you can lean on. Android and Wasm have compile coverage only. OpenBSD has a human clicking a button. If your product ships on one of the first group you can treat the framework as tested; on the other two, treat it as compiling, and check your own suite on real devices before you depend on it.

No dependency to declare, but a nightly to build from

Two statements in the same README sit next to each other without either being wrong. The first is that Swift Testing is included with the Swift 6 toolchain and Xcode 16, and that you do not need to add it as a package dependency to your Swift package or Xcode project. There is a bare import and nothing else:

swift
import Testing

The second is an IMPORTANT block warning that Swift Testing depends on upcoming language and compiler features, so if you are building it from source the main branch requires a recent main-branch development snapshot toolchain. The repository's own Dockerfile backs that up, building on a nightly main image rather than a tagged release:

dockerfile
FROM swiftlang/swift:nightly-main-jammy

Both facts describe the same project from two positions. As a consumer you get a framework already inside the toolchain, so there is no version to pin and nothing in your dependency graph. As a contributor you are working against language features that have not shipped, so you need a snapshot compiler, and the Dockerfile creates a `build-user` with uid 998 to match the Swift.org continuous integration setup.

The practical rule is straightforward. Use the copy inside your toolchain for anything you ship. Read main only when you need a behaviour your installed toolchain does not have yet, and if you do, expect to move to a new release and expect a breaking change, because the 6.4 release notes include a commit whose stated purpose is removing a separate `accessorDecl` declaration from macro expansions. Macro internals are not a stable surface, and a main branch snapshot has no promise attached to them at all.

Release tags carry the toolchain version, not a library version

The three most recent releases are tagged `swift-6.4.0-RELEASE` from 2026-09-15, `swift-6.3.2-RELEASE` from 2026-05-13, and `swift-6.3.1-RELEASE` from 2026-04-21. The naming pattern is the finding. These tags track the Swift toolchain they ship in, which is why the repository carries a `VERSION.txt` and a `Package.swift` at the same time as a convention where the release number is somebody else's version number. When you read a changelog entry, the number tells you which toolchain the change landed in, not how many releases the testing library itself has had.

That convention makes one pair of entries look odd. The 6.3.1 notes contain a change that fully qualifies the reference to the `Comment` type in the `#expect` expansion when the comment argument is not a string literal, and the 6.3.2 notes contain the same change again, filed against a different pull request number. The same fix, twice, in consecutive patch releases, is the signature of a first attempt that did not reach everyone who needed it. If you are on 6.3.1, assume the macro bug is still there.

The 6.4 notes are broader: official distribution locations are documented along with the guidance to use a built-in copy, issue attribution now works when tests run in detached tasks, and there is new documentation for debugging Swift Testing tests with LLDB from the command line. That last one pairs with the badges at the top of the README, which point at two workflows, one using the main toolchain and one using the 6.3 toolchain. The 6.3 badge is now behind the newest tag, so continuous integration is verified against more than one compiler version and the older one has not been bumped in the badge yet. The repository is Apache 2.0, with `LICENSE.txt`, `NOTICE.txt`, a `.licenseignore` and a `.license_header_template` in the tree, so the license text is enforced in continuous integration rather than left to review.

Editorial conclusion

Swift Testing is the framework that arrived with Swift 6 and Xcode 16, and the strongest reason to adopt it is that there is nothing to install. A failed #expect prints the expression with the values it actually compared, which shortens the loop between running a suite and understanding what broke. Traits give you runtime conditions and time limits, tags give you a selection handle, and arguments turn one test body into a table of cases. Two things deserve attention before you rely on it. The support table marks Wasm and Android as build only, which means their tests are never executed in CI, and the release tags follow the toolchain version rather than a library version, so 6.3.1 and 6.3.2 shipped the same expectation macro fix twice. Use the copy inside your toolchain, not a main branch build.

Frequently asked questions

Do I need to add Swift Testing as a package dependency?

No. The README states that Swift Testing ships with the Swift 6 toolchain and Xcode 16, so importing the Testing module is enough and no dependency goes in your Package.swift. That applies to using the library; building it from the main branch does require a recent main-branch development snapshot toolchain.

How does Swift Testing compare to XCTest?

The README says XCTest tests can run side by side with Swift Testing tests, so migration is incremental. The practical differences are that assertions are macros that report the compared values on failure, test declarations are plain functions rather than classes, and tests run in parallel by default.

Which platforms does Swift Testing actually run its tests on?

Apple platforms, Linux, FreeBSD and Windows are Supported with Automated qualification, meaning continuous integration builds and passes all tests. Wasm and Android are Experimental and Build Only, and OpenBSD is Experimental with Manual qualification, so their tests are not executed automatically.

How do I run one test many times with different inputs?

Pass an arguments list to the Test attribute. Each element becomes its own case against the same function body, so a failure names the input that produced it.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. swiftlang/swift-testing on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/swiftlang-swift-testing.svg)](https://hysenlabs.com/projects/swiftlang-swift-testing)