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
Migrating Async Methods
Testing async methods with Swift Concurrency has no magic trick to it. You just mark the test method async and you’ll be able to call and test any async methods inside the body.
Find test_submit_empty_order_surfaces_message and replace with the following code:
@Test @MainActor func submitEmptyOrderSurfacesMessage() async {
Like before, Xcode sees @Test and recognizes this method as a test method. As before, you could click the diamond button on the line of the method declaration to already run the test successfully. But you’ll go farther and migrate the test to use Swift Testing constructs.
Find the following code:
XCTAssertNil(model.receipts.last)
XCTAssertEqual(model.errorDescription, "Your order is empty.")
And replace with the following:
#expect(model.receipts.last == nil)
#expect(model.errorDescription == "Your order is empty.")
Run the unit test to make sure it’s passing.

Just like that you’ve migrated your first set of tests from XCTest to Swift Testing!
Running Unit Tests in Serial
By default, Swift Testing runs tests concurrently to take less time and maximize developer efficiency. However, there are cases when you might need to run tests in serial, meaning the test functions run one-by-one in order instead of all at the same time. To make a suite of tests run in serial you use the serialized trait.
@Suite(.serialized)
struct OrderModelTests {
You’ll learn more about test traits in a bit, but before that, you’ll finish migrating this file.
Migrating XCTUnwrap Tests
A common type introduced by Swift is the Optional: a type whose value is either populated or nil. Optionals in application code are unwrapped before use to guarantee there’s a value at run time for subsequent code to use. In test code, you could use use #expect(_:_:sourceLocation:) to assert a value isn’t nil then force unwrap it subsequently. However, there is a better way to unwrap test values while still expressing that a nil value means a test failure.
With XCTest, that’s where XCTUnwrap comes in. It requires that the value is not nil and unwraps it. If the object is nil, the test stops and fails on that line.
You’ll learn how to migrate these kinds of unit tests now.
Find the next unit test method, test_adding_out_of_stock_kind_surfaces_a_message_and_adds_nothing and start by replacing the declaration of the method with the following:
@Test func addingOutOfStockKindSurfacesAMessageAndAddsNothing() throws {
Next, find the following line:
// 1
let error = try XCTUnwrap(model.error)
And replace with the following:
let error = try #require(model.error)
The new require(_:_:sourceLocation:) macro unwraps any optional value and ensures the object exists. If model.error is nil, the macro will throw an error and fail the test.
You can also use this macro to assert other expectations that absolutely must be true to continue that unit test, and that you want to stop and fail when they don’t.
Replace the rest of the code inside the test method with the following:
#expect(
error as? OrderError ==
OrderError.outOfStock(.latte)
)
#expect(
model.errorDescription ==
"Latte is out of stock."
)
Run the unit tests to make sure everything is correct.

Putting Everything Together
Before you move on, there’s one last unit test in this file that you’ll migrate. It uses everything that you learned so far.
Inside OrderModelTests.swift, find the final unit test method named test_submit_success_publishes_receipt_and_clears_items.
Replace its declaration for the following:
@Test func submitSuccessPublishesReceiptAndClearsItems() async throws {
Then, inside the method, find the code that unwraps the latest receipt and asserts its values and replace for:
let receipt = try #require(model.receipts.last)
#expect(receipt.itemCount == 2)
#expect(receipt.total == 7.75)
#expect(model.isOrderEmpty == true)
#expect(model.errorDescription == nil)
#expect(model.isSubmitting == false)
Run the unit tests.

Great job! You finished migrating everything from OrderModelTests. All unit tests are passing and you learned that it’s quite straightforward to migrate tests written with XCTest to Swift Testing.
Before you work on migrating more tests, there’s one last thing to do in this file. You no longer need to import XCTest as the framework is no longer in use.
Find this line at the top of the file:
import XCTest
And just remove it.
Run the entire test suite by clicking the diamond button to make sure all unit tests are passing.

Migrating CoffeeShopTests
You’re halfway there. OrderModelTests.swift has been migrated to use the latest and greatest Swift Testing. And the best part is, because Swift Testing is interoperable with XCTest, you can still run all the tests in the project despite using both frameworks.
Now, you’ll migrate CoffeeShopTests.swift, the file that tests the CoffeeShop object which is responsible for creating an order, submitting orders to the barista and brewing coffee.
Migrating Set Up and Tear Down Methods
In Xcode, open CoffeeShopTests.swift and take a look at the code.
CoffeeShopTests builds its tests by using setUp() and tearDown() methods. These methods are run in XCTestCases before every unit test is executed. Here, it instantiates a new CoffeeShop object and after the test is done it deallocates it.
You’ll start by migrating this functionality.
First, import the Swift Testing framework at the top of the file:
import Testing
Next, replace the class declaration for the following:
@Suite
final class CoffeeShopTests {
The compiler is going to complain about CoffeeShopTests overriding setUp() and tearDown() methods. That’s because CoffeeShopTests no longer inherits from XCTestCase.
To fix that, replace setUp() with:
init() {
shop = CoffeeShop()
}
And replace tearDown() with:
deinit {
shop = nil
}
Notice that unlike OrderModelTests, you’re leaving CoffeeShopTests a class type. @Suite can be added to any type, and the way Swift Testing migrates XCTest’s setUp and tearDown methods is by using the init and deinit of a class type.
Migrating Callback Methods
With Swift Concurrency, methods that use callbacks to indicate the ending of an asynchronous operation have pretty much been replaced with the async/await pattern. However, every now and then, a callback closure is useful. Take a look at the code inside test_brew_order_reports_progress_for_each_coffee.
CoffeeShop.brewOrder(items:onProgress:) is a method that takes an array of orders and starts brewing them immediately. It has an onProgress closure that gets called whenever an item from the order is done. This is useful when you need to indicate how much of the order is done and show to the user that the operation is progressing.
XCTest uses expectation(description:) and fulfillment(of:timeout:enforceOrder:) methods to handle these situations. You’ll learn how to migrate this to Swift Testing now.
Replace the method declaration for the following:
@Test
@MainActor
func brewOrderReportsProgressForEachCoffee() async throws {
Now, find the following code:
let expectation = expectation(
description: "brewOrder reports progress once per coffee"
)
expectation.expectedFulfillmentCount = 3
let receipt = try await shop.brewOrder(items: order) { _ in
expectation.fulfill()
}
await fulfillment(of: [expectation], timeout: 1)
and replace it with:
// 1
let receipt = try await confirmation(
"brewOrder reports progress once per coffee",
expectedCount: 3
) { brewed in
// 2
try await shop.brewOrder(items: order) { _ in
// 3
brewed()
}
}
Here’s an explanation of the code above:
- Here you use
confirmation(_:expectedCount:isolation:sourceLocation:_:)to open a closure where the actual method is going to be tested. You pass a message and the expected number of times the callback must be called for this confirmation to pass - Then, inside the closure, you call
brewOrder(items:onProgress:)passing the array of orders - Finally, inside
onProgressclosure, you call the confirmation to tell the method that a coffee has been brewed
The method waits for the confirmation to happen 3 times, and if that doesn’t happen it fails.
Next, replace the final line of the method with the following:
#expect(receipt.itemCount == 3)
Finally, run the unit tests and make sure this method is passing.

Amazing! Your tests are passing and you’re one step closer to finishing the migration.
In the next section, you’ll learn how to fail a unit test whenever a necessary condition is not met.