Swift Testing is the new framework for unit testing in Swift. It is shipped as the default in Xcode 16 (now in beta), and it looks great.
Watch the session video from WWDC 2024 for the official intro from Apple.
Swift Testing is an open source framework hosted on GitHub. You can read much more about it in the main project, and also in the Vision Document.
The @Test Attribute
There are four main building blocks in Swift Testing, and the first is the @Test
attribute. In XCTest you needed a test
prefix on your method name to tell the framework that this method is a test. The @Test
attribute does that now, so your test function can have any name.
A test can also now be global. In XCTest, tests had to belong to a subclass of XCTestCase
. (Talking of which, we'll get to test suites soon.)
import Testing
@Test func myGlobalFunction() {
#expect(true)
}
Expectations
The second building block is Expectations. These are equivalent to assertions in XCTest, but while XCTest has lots of different assertions (XCTAssert
, XCTAssertTrue
, XCTAssertEqual
, XCTAssertNotNil
etc.), but Swift Testing has just two: #expect
and #require
.
The #expect
macro lets you specify any condition, using the standard Swift language features. If the condition evaluates to true, the test passes.
The #require
macro also validates a condition, but in this case, if the condition evaluates as false, the test will terminate immediately. #require
also throws an exception, so you need to call it with try
.
One tool of XCTest that I use a lot is XCTUnwrap
, to safely unwrap an optional, and fail the test if the optional can't be unwrapped. You can use #require
the same way:
let unwrappedValue = try #require(myOptionalValue)
If the test fails, then not only will it fail, it will also generate an error message using the evaluated expression, to help you see why it failed. This is a huge difference from having to generate failure messages manually in XCTest.
Traits
The third building block of Swift Testing is Traits. Traits let you add details to a test, for example a display name, a bug ticket reference, or a condition under which the test should run (e.g. only in a certain language, or only when a certain feature flag is enabled).
This example shows enabling a test only when a feature is enabled. (It's taken directly from the Swift Testing README.)
@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!"))
}
Tags
You can also use Traits to organise tests into tags, and you can group tests by tag rather than suite or file in the Xcode sidebar. I love this idea for small projects that have a limited number of people working on them. I know it’ll be misused in bigger teams, with lots of different tags being semantically the same.
Parameterised Tests
It can be common to run one test multiple times, changing the parameters to test different conditions. Traits let you simplify that logic, by defining your arguments outside of the test. You can then see the different test runs in the sidebar in Xcode, and even re-run specific arguments, while only having one test defined.
Here's an example, again taken from the Swift Testing README:
@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)
}
This runs the test against three different videoName
values: "A Beach", "By the Lake" and "Camping in the Woods". You'll see each of these in the sidebar as different tests, but you only need to write one @Test
function.
Test Suites
Test Suites still exist, and they’re the fourth building block of Swift Testing. Instead of a subclass of XCTestCase
, you can keep it really simple and embed your tests in a struct. That alone creates a Suite, implicitly.
struct MyTests {
@Test func myTest() {
#expect(true)
}
}
MyTests
in the example above is a Test Suite. This is inferred from the presence of a @Test
.
You can also use the @Suite annotation. One reason why you might do that explicitly is to apply Traits that should be inherited by all tests. For example, if you apply a Tag to a Suite, that Tag applies to all tests in the Suite.
Suites can be a struct, actor or class, but struct is encouraged. This means you get the benefit of value semantics; each test will get its own instance of the Suite. That means you don't risk sharing data between tests and corrupting the tests.
One reason you may use choose to use a class or actor, however, is if you need a deinit
method. In XCTest, you had setUp
and tearDown
, and multiple examples of each, depending on whether you wanted it to be run on a class or instance level, or if it should be async
. There's only one init
and deinit
in Swift Testing, and deinit
isn't available in structs.
Wrap Up
XCTest isn't going anywhere any time soon. You can take any migration slowly, and use both XCTest and Swift Testing in the same test target. And Apple explicitly mentions that XCTest is still required for performance and UI testing.
But Swift Testing looks like a great improvement on unit testing, and I'm excited to try it out.