Home Articles

Modernizing Your Tests: A Practical Guide to Swift Testing Framework

Modernizing Your Tests: A Practical Guide to Swift Testing Framework

Modernizing Your Tests: A Practical Guide to Swift Testing Framework

XCTest vs Swift Testing Comparison

No More setUp() – Use init Instead

XCTest

Test setup typically goes into an overridden setUp() method:

class FoodTruckTests: XCTestCase {
    var batteryLevel: NSNumber!
    override func setUp() async throws {
        batteryLevel = 100
    }
    ...
}

XCTest Example

Swift Testing

Setup is cleaner and Swift-native:

struct FoodTruckTests {
  var batteryLevel: NSNumber
  init() async throws {
    batteryLevel = 100
  }
  ...
}

Swift Testing Example

No special methods required — just use the init initializer to prepare test state.

Replace tearDown() with deinit

XCTest

teardown looks like this:

class FoodTruckTests: XCTestCase {
    var batteryLevel: NSNumber!
    override func setUp() async throws {
        batteryLevel = 100
    }
    override func tearDown() {
        batteryLevel = 0 
	// drain the battery
    }
    ...
}

XCTest Example

Swift Testing

You can use Swift’s native deinit:

final class FoodTruckTests {
  var batteryLevel: NSNumber
  init() async throws {
    batteryLevel = 100
  }
  deinit {
    batteryLevel = 0 
    // drain the battery
  }
  ...
}

Swift Testing Example

Cleaner Assertions and Flexible Test Naming

Here’s how a typical XCTest case might look:

func testEngineWorks() throws {
  let engine = FoodTruck.shared.engine
  XCTAssertNotNil(engine.parts.first)
  XCTAssertGreaterThan(engine.batteryLevel, 0)
  try engine.start()
  XCTAssertTrue(engine.isRunning)
}

XCTest Example

In Swift Testing, the same test is more expressive and lighter:

@Test func engineWorks() throws {
    let engine = FoodTruck.shared.engine
    try #require(engine.parts.first != nil)
    #expect(engine.batteryLevel > 0)
    try engine.start()
    #expect(engine.isRunning)
}

Swift Testing Example

Key improvements:

  • #require safely unwraps and stops the test if needed.

  • #expect replaces multiple XCTAssert calls with one unified syntax.

  • You’re no longer forced to prefix functions with test.

Improvements

Improvements when compared to XCTest

Improvements when compared to XCTest

Clean, Swifty Syntax with Macros

The introduction of @Test, #expect, #require, and traits like .tags, .timeLimit, and .bug makes your tests feel like first-class Swift code.

  • No more boilerplate test classes

  • No forced naming conventions

  • Your test code looks and feels like Swift — clean and expressive

Improved Readability

Thanks to these macros, it’s much easier to understand what each test is doing at a glance. Descriptive traits and display names make your intent crystal clear.

@Test("Validates new checkout flow", .tags(.regression, .payment))

Readability Example

Less Boilerplate

No need to write setUp() or tearDown() if you don’t need to. Use init and deinit like you would in regular Swift types.

Tests can now be written in:

  • Global functions

  • Structs

  • Final classes

  • Even actors

SPONSOR

iOS Conference AI Image

The Unique iOS Swift Conference in the UK

SwiftLeeds is a premier iOS conference taking place on October 7-8 this year. If you’re looking to stay updated on the latest trends in iOS development and connect with like-minded professionals, this event is a must-attend! Don’t miss out—book your tickets now!

Get your tickets!

Actionable Failures

Failures in Swift Testing are designed to be helpful. You get better failure messages with context, and in many cases, suggestions or expectations clearly spelled out — not just red error lines.


 Problem with XCTest

class UserModelXCTests: XCTestCase {
    func testUserProfileUpdate() {
        var user = User(name: "John", age: 25, email: "john@example.com")
        user.updateProfile(name: "John Doe", age: 29)
        // On failure, only shows true != false, not the actual values
        XCTAssertTrue(user.name == "John Doe" && user.age == 26)
    }    
}

XCTest Example

Error:
XCTAssertTrue Failed

When this fails, the output is simply:
true is not equal to false

Hence, it’s not very helpful.


Swift Testing to the Rescue

struct UserModelTests {
    @Test func userProfileUpdate() {
        var user = User(name: "John", age: 25, email: "john@example.com")
        user.updateProfile(name: "John Doe", age: 29)

        // On failure, shows the full expression and all values
        #expect(user.name == "John Doe" && user.age == 26)
    }
}

Swift Testing Example

On failure, Swift Testing provides detailed expression breakdowns:

⚠️

Expectation failed: (user.name == "John Doe" → true) && (user.age == 26 → false)

You’ll also see a “Show” button to expand a full detailed view with all values involved in the condition.

Failure message

Failure message

Detailed Error Message

Detailed Error Message

Actionable Failures

Actionable Failures

Migrating a Test from XCTest

Swift Testing can coexist with XCTest

Both can live under the same unit test target, making gradual migration easy.


What to Migrate and How

  • Simplify old XCTestCase classes with only one test → Move to a global @Test function

  • Consolidate multiple similar XCTests → Use parameterized tests for cleaner logic

  • Function naming is flexible → The word test at the beginning is no longer required

Continue Using XCTest For:

  • XCUIApplication-based UI automation

  • Performance testing with XCTMetric

  • Tests that must be written in Objective-C


Avoid Mixing Assertion Styles

Don’t mix Swift Testing’s #expect/#require with XCTAssert

Each framework has its own lifecycle and assertion style — mixing leads to unexpected behavior

Note

Please find the GitHub Repo

Please refer the previous article on Swift Testing Overview

To learn more about the interesting sections in Swift Testing

This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.