Moving from XCTest to Swift Testing
Swift Testing is Apple’s replacement for the objective-C XCTest unit testing framework. Discover how to migrate your existing XCTest suites over to Swift Testing, including how to get some assistance from Xcode’s agentic AI tooling. By Renan Benatti Dias.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Moving from XCTest to Swift Testing
35 mins
- Getting Started
- Updating Your First Unit Test
- Importance of Unit Testing
- Updating OrderModel
- Understanding @Suite
- Understanding @Test
- Updating Assertion Functions
- Migrating Async Methods
- Running Unit Tests in Serial
- Migrating XCTUnwrap Tests
- Putting Everything Together
- Migrating CoffeeShopTests
- Migrating Set Up and Tear Down Methods
- Migrating Callback Methods
- Failing Tests on Purpose
- Asserting Error Types
- Testing Methods That Should Not Throw Errors
- Using Traits
- Disabling and Enabling Unit Tests
- Tracking Bugs
- Limiting Test Run times
- Tagging Tests
- Using Parameters to Test Permutations
- Using Xcode 27 AI Skill to Migrate Unit Test
- Where to Go From Here
Failing Tests on Purpose
Find the test method test_submit_empty_order_throws_empty_order. This test purposefully calls submitOrder(items:) with invalid input so that the method throws an error. The idea of this unit test is to check whether the method throws the correct error when given an invalid order.
When the expected error doesn’t occur, you need a way to tell the framework that this test failed. To do that, you use XCTFail(_:file:line:) to force a failure when the error isn’t thrown.
Replace the method declaration with the following code:
@Test func submitEmptyOrderThrowsEmptyOrder() async {
Next, find the code, inside the do clause, that uses XCTFail(_:file:line:) to fail the unit test and replace with the following:
Issue.record(
"Expected submitOrder() to throw OrderError.emptyOrder"
)
Here, you replace XCTest’s XCTFail(_:file:line:) with Issue.record(_:severity:sourceLocation:). This class method also takes a string with a message explaining why this test failed.
Before you move on, replace the code inside the catch clause with:
#expect(error as? OrderError == .emptyOrder)
Run the unit test.
![]()
Note that Issue.record(_:severity:sourceLocation:) does not stop a test execution. If you wish to do so you must either return after calling this method, or you can use require(_:_:sourceLocation:).
While Issue.record works great, Swift Testing has a cleaner way of checking if an error is thrown.
Asserting Error Types
Find the next test method named test_adding_invalid_quantity_throws and take a look at its code.
This test method purposefully makes a similar mistake to the previous one. It calls createOrder(_:size:quantity:) with an invalid quantity of 0.
Here, the test uses XCTAssertThrowsError(_:_:file:line:_:) to assert an error is indeed thrown by createOrder(_:size:quantity:). It also gives you back the error that was thrown in a closure so that you can assert it is OrderError.invalidQuantity(0)
Replace the declaration of the method with the following:
@Test func addingInvalidQuantityThrows() {
Next, replace the code inside the method with:
// 1
#expect(throws: OrderError.invalidQuantity(0)) {
// 2
try shop.createOrder(
.espresso,
size: .small,
quantity: 0
)
}
Here’s how this works:
- You use
expect(throws:_:sourceLocation:performing:)and tell the macro what error you’re expecting. In this caseOrderError.invalidQuantity(0). - Then, you simply call the method that throws this error inside the closure
The macro makes sure the method throws that specific error, and if that doesn’t happen it fails the test.
Run the unit test to make sure it’s passing.
Testing Methods That Should Not Throw Errors
You just tested a method’s thrown error by calling it with invalid input. But, you may also want to test the same method and ensure it never throws an error given valid input.
The next test, test_adding_available_kind_does_not_throw uses XCTAssertNoThrow(_:_:file:line:) for exactly that purpose. It makes sure that the method does not throw an error.
You’ll migrate that now.
Rename the method name to use camel case and remove the test_ prefix from it. Also add the Test(_:) macro to the method.
Next, replace the code of the method with the following:
// 1
#expect(throws: Never.self) {
// 2
try shop.createOrder(
.espresso,
size: .small,
quantity: 1
)
}
Here’s the breakdown:
- You’re using
expect(throws:_:sourceLocation:performing:)again but this time you’re using the typeNeverto indicate this method may not throw an error - You call the method with valid inputs
Run the unit test and make sure the method is not throwing an error.
![]()
Very good!
This works great for checking that a method doesn’t throw an error; but, you may also want to check the results of a method.
For the final unit test in this file, you’ll test the submission of an order.
Find test_submit_order_generates_receipt:
and replace the method declaration with the following:
@Test
@MainActor
func submitOrderGeneratesReceipt() async throws {
Next, find the following line:
XCTAssertEqual(receipt.itemCount, 1)
And replace it with:
#expect(receipt.itemCount == 1)
The test calls createOrder(_:size:quantity:) directly. If the method throws an error, the test will stop and fail on that line. After that, it calls submitOrder(items:) with the created order and also waits for its response. Finally, the test checks that the receipt item count is 1.
Run the unit test to make sure everything is correct.

Now, all that is left is to remove the XCTest import declaration from the top of the file. Run the entire test suite to make sure everything is passing.

Great job! You finished migrating all the unit tests of SwiftBrew to use Swift Testing.
Swift Testing is simple and can be adopted gradually in your project. And while you’re done migrating the tests of this project, the framework has a couple more tricks up its sleeve to power up your unit tests.
Using Traits
Swift Testing introduced something XCTest doesn’t have: traits. You can use traits to annotate or modify both individual test methods or an entire suite.
Disabling and Enabling Unit Tests
From time to time, you might come across intermittent or “flaky” unit tests. Those are unit tests that most of the time pass, but for some reason – a race condition, a specific configuration, etc. – they fail now and then. And while the correct course of action would be to debug and find out what’s causing this test to fail, you might not always have the time to do so.
With that in mind, Apple has some handy traits for you to use. Instead of commenting out a unit test and leaving it to fix later, you can use the disabled(_:sourceLocation:) trait to skip a test.
@Test(.disabled("Test is failing because of some race condition"))
@MainActor
func brewOrderReportsProgressForEachCoffee() async throws {
disabled(_:sourceLocation:) optionally takes a string, that will be recorded in the test report explaining why this test was skipped.
![]()
You can also skip a unit test with some pre-condition, like a feature flag:
@Test(.disabled(if: FeatureFlag.isBrewProgressEnabled == false))
@MainActor
func brewOrderReportsProgressForEachCoffee() async throws {
And Swift Testing also has the opposite trait, enabled(if:_:sourceLocation:), that you can use exactly like its counterpart, just with the conditional flipped.