Introducing Swift Testing

John Gers
6 min readJul 6, 2024

Prerequisites:

  • Swift 6
  • Xcode 16

To use Swift Testing, your target needs to be compiled using Swift 6 and you need to be using Xcode 16.

What is Swift Testing?

Swift Testing is the new open source testing framework for testing Swift code.

  • It is cross platform. Which means it can be used on all Apple platforms as well as Linux and Windows
  • Tests can now be ran in parallel on all physical devices for the first time ever
  • Works seamlessly with Xcode cloud integration as you can view results in App Store Connect
  • Tests runs are parallelized by default

If you need help getting your tests to run in parallel, Swift 6 can help with the new Data Race Safety. Or you can adopt it incrementally and use the .serialized tag.

@Suite

Suites allow you to group your tests together.

Create a test suite by marking your class, struct, or actor with the @Suite macro
  • Each test that runs, runs in it’s own Suite instance
Just like in XCTest with XCTestCase, Swift Testing will create a new instance of the Suite for each method
  • Can have subsuites
To create a subsuite, just add the @Suite macro to your nested type. Subsuites will inherit traits from their parent
  • Traits
A variety of traits can be added at the suite level
To use more than one tag, just pass in as many as you need. Note: The .comment tag is now depreciated in favor of just using a regular comment.

@Test

Create a test by marking your function with the @Test macro
  • Freestanding
Tests no longer need to exist inside of a test suite instance and can be freestanding functions
  • Tags
You can use tags with tests just like suites. Tests will inherit the tags of its suite (if there is one) Note: The .comment tag is now depreciated in favor of just using a regular comment.

CustomTestStringConvertible

You can conform a type to CustomTestStringConvertible
This gives you a more detailed description in the test navigator and test report
You can pass an array of arguments to your tests and each case will run even if one before it fails

Allows you to test all arguments rather than failing for the first argument that fails.

Because we’re asserting using require, our code can only test the first number in the for loop
Solution: Use the parameterized test capability of Swift Testing to test all cases
Left: Each test case runs independent and are parallelized by default for parametrized tests. Right: Use the .serialized tag to change this and make them run independently but in order.
You can pass up to a maximum of two collections to test all combinations. Two is the limit because of exponential growth.
If you just wanted to test each element with it’s pair one by one, you can transform your data using zip

Traits

  • Serialized
Since Swift Testing is parallelized by default, you add .serialized to run your tests one at a time.

Note: that all of the following can be used on tests or suites

  • Tags
You can define a custom tag by extending Tag and applying it to your test or suite
Custom tags allow you to relate tests across suites
And allows you to run those related on their own using the project navigator
Using the test plan, you can include/exclude tests by their tags
  • Enabled
Enabled allows you to use an expression to determine if your test should run
  • Disabled
disabled allows you to disable tests with a descriptive message
  • TimeLimit
timeLimit allows you to set a time limit on your test execution
  • Bug
bug allows you to associate a URL. Note: the url needs to be valid for the macro to process it correctly.
  • Serialized
Since Swift Testing is parallelized by default, you add .serialized to run your tests one at a time.

Confirmation

Use confirmation for tests than need to invoke a callback/asynchronous functions

Can specify an expectedCount

Note that your confirmation block can have an expectedCount, which means if we invoke the confirmation more than once, it will fail because it only expects one.

Swift Testing for those familiar with XCTest

Swift Testing is the successor to the existing XCTest framework and the good news is that if you already know XCTest, learning Swift Testing will be a breeze.

How to migrate from XCTest to Swift Testing

Swift Testing and XCTest can exist in the same test target or test plan so you can begin migrating once your target is compiled with Swift 6 and you’re using Xcode 16.

  • Swap import XCTest for import Testing
  • Swap XCTAssert… for #expect
Swap XCTAssert… for #expect
The expect macro eliminates the need for individual functions and can work with any expression
  • Swap XCTUnwrap for #require
  • Swap setup/teardown for init/deinit
Consider using factory methods instead of setup/teardown across Swift Testing and XCTest to simplify testing the whole object lifecycle
  • Swap expectations for confirmations
Similar to other Async/Await APIs, you need to wrap your code in a confirmation block
Note that your confirmation block can have an expectedCount, which means if we invoke the confirmation more than once, it will fail because it only expects one.
  • Swap XCTFail for Issue.record
  • Swap continueAfterFailure for #require
Setting continueAfterFailure to false or using a failing #require and will halt the test. Without this, both assertions or #expect results will be reported
  • Swap XCTSkip for @Suite(.disabled(“Message”))
You can either disable it at the Suite level will all the tests in that suite will be disabled or apply it directly to a test
  • Swap XCTExpectFailure for withKnownIssue()
Note: When using XCTExpectFailure or withKnownIssue, the failure won’t fail your test suite

When to continue using XCTest

  • When using UI automation APIs like XCUIApplication
  • When using performance testing APIs like XCTMetric
  • If your tests can only be written in Objective-C

--

--