QApilot - AI-Powered Mobile App Testing
    Back to Blogs
    XCTest Guide: Setup, Lifecycle and XCTest vs Swift Testing - QApilot Blog

    XCTest Guide: Setup, Lifecycle and XCTest vs Swift Testing

    Summarise with AI

    Key takeaways

    Set up an XCTest target in Xcode, learn the setUp and tearDown order, test callbacks with expectations, see how UI tests differ, and decide which unit tests move to Swift Testing on Xcode 16 through 27.

    How-To Guidesxctestxctest vs swift testingswift testing vs xctestxctest setupxctest frameworkwhat is xctestswift testingunit testing

    Harini Mukesh

    Product Marketing Analyst

    14 min read

    XCTest is Apple's testing framework for writing and running unit tests, performance tests and UI tests for Xcode projects, using test methods inside XCTestCase subclasses that check results with XCTAssert functions. Since Xcode 16, Apple also ships Swift Testing and recommends it for new unit tests, while UI tests and performance tests stay on XCTest.

    XCUITest is the UI-testing part of XCTest, explained briefly below and in depth in our XCUITest guide. This page covers target setup, the lifecycle order, async expectations, XCTest vs XCUITest, XCTest vs Swift Testing, migrating unit tests, and where the upkeep in an iOS suite really lands, using a recipe app called Pantry as the running example.

    What is XCTest?

    Every test bundle Xcode creates links against XCTest, and a test inside it is an instance method on an XCTestCase subclass that takes no parameters, returns nothing and starts with the lowercase word test, which is how Xcode finds it without any registration step. Results are checked with XCTAssertEqual, XCTAssertNil, XCTUnwrap and the rest of the family, as the XCTest documentation describes.

    In practice the framework covers three jobs. Unit tests call your types directly, such as asserting that a Cart holding a 250-cent and a 125-cent item totals 375. Performance tests wrap a block in measure and flag a regression when decoding a 2,000-recipe JSON feed slows down. UI tests launch the built app and tap through it from outside, a split explained further down. If isolating one function from the network is new to your team, start with our primer on unit testing for mobile apps.

    How do you set up XCTest in Xcode?

    Choose File > New > Target, pick Unit Testing Bundle, point Target to be Tested at your app, and select XCTest as the testing system. That last choice matters: Swift Testing became the template's default in Xcode 16, so skipping it gives you @Test functions instead of an XCTestCase class. Then import your app module with @testable so internal types are visible, and write one method:

    import XCTest
    @testable import Pantry
    
    final class CartTotalTests: XCTestCase {
        func testTotalAddsLineItems() {
            let cart = Cart(items: [Item(priceInCents: 250), Item(priceInCents: 125)])
            XCTAssertEqual(cart.totalInCents, 375)
        }
    }
    

    Press Command-U (Product > Test) or click the diamond beside a method in the Test navigator. From Terminal, xcodebuild test -scheme Pantry runs the active test plan, -only-testing PantryTests/CartTotalTests/testTotalAddsLineItems narrows it to one method, and -run-tests-until-failure -test-iterations 20 hammers a test that fails once a week on the build server. Each xcodebuild run writes an .xcresult bundle you can archive as a pipeline artifact, as our guide to running mobile tests in CI/CD shows; Apple lists the flags in Running tests and interpreting results.

    Test plans (.xctestplan files) decide which targets and configurations a scheme runs, so a smoke plan can skip slow suites while a nightly plan runs everything, the executable side of a written iOS test plan. Swift packages skip the template: put files under Tests/PantryCoreTests and run swift test.

    How does the XCTest lifecycle work?

    XCTest calls a fixed sequence of hooks around every test method, and that order explains most leaking-state bugs. The table follows Apple's article on setting up and tearing down state.

    The XCTest test lifecycle, in order

    Step Method Runs Use it for
    1 class func setUp() Once, before the first test in the class Costly shared state such as copying a seed SQLite file (no assertions here)
    2 setUp() async throws, then setUpWithError(), then setUp() Before every test method A fresh Cart or mock store for each test; assertions allowed
    3 func test…() The test itself Call the code under test and assert on the result
    4 Teardown blocks added with addTeardownBlock After that test, last-in first-out Delete the temp folder this one test created
    5 tearDown(), then tearDownWithError(), then tearDown() async throws After every test method Reset per-test state; skipped if the test crashes
    6 class func tearDown() Once, after the last test in the class Remove the seed database copied in step 1

    Three rules follow from that order, and each one prevents a common leak. Put assertions in the instance hooks, because the class-level setUp() and tearDown() have no test instance to report against. Treat teardown as cleanup for the current test only: after a crash XCTest never reaches it, so each test should build its own starting state in setUp. And continueAfterFailure = false stops a test at its first failed assertion, useful when later checks would trip over a nil value, while teardown still runs afterwards.

    How do you test asynchronous code with XCTest?

    For code written with async and await, mark the test async throws, call try await, and assert on the result; XCTest waits for the call to finish. Older callback-style code needs expectations instead of a plain await. Pantry's RecipeLoader answers through a completion handler, and the same pattern fits delegate callbacks, background queues, Combine publishers and Objective-C APIs. Create an XCTestExpectation, call fulfill() when the callback lands, and set a timeout:

    func testLoaderReturnsRecipes() async {
        let arrived = expectation(description: "recipes arrive")
        RecipeLoader().fetch { recipes in
            XCTAssertFalse(recipes.isEmpty)
            arrived.fulfill()
        }
        await fulfillment(of: [arrived], timeout: 2)
    }
    

    In an async test, use await fulfillment(of:timeout:), which Apple calls the concurrency-safe alternative to the blocking wait(for:timeout:); the older call still suits synchronous tests. Both appear in Asynchronous tests and expectations. If a callback should fire once per page of three search pages, set expectedFulfillmentCount = 3, and set assertForOverFulfill = false when extra calls are harmless.

    Actor isolation also needs a look once tests start using async code. Per Apple's migration guide, XCTest runs synchronous test methods on the main actor by default, while async tests and async setUp or tearDown run on an arbitrary actor unless marked @MainActor, so a test touching a main-actor view model needs that annotation.

    XCTest vs XCUITest: what's the difference?

    XCUITest is not a separate framework. It is the name people give to the UI-testing part of XCTest, and the split shows up in which bundle you create. A Unit Testing Bundle is loaded alongside your code, so @testable import Pantry lets a test call Cart or RecipeLoader directly and finish in milliseconds. A UI Testing Bundle cannot see your types at all. It runs in its own test-runner process and controls the installed app from the outside through XCUIApplication, tapping buttons and reading labels the way a person would.

    Apple now documents those UI types under a separate XCUIAutomation framework (Xcode 16.3 and later), yet they still arrive with import XCTest, and UI tests are still written as XCTestCase subclasses. Because UI tests launch the real app, they run slower and depend on screen layout, which is why most teams keep many unit tests and fewer UI tests. Target setup, element queries and keeping UI runs stable are all covered in our XCUITest guide.

    XCTest vs Swift Testing: which should you use?

    Use both, split by test type. On Xcode 16 or later, write new unit and integration tests in Swift Testing; keep XCTest for UI tests, performance tests and any test that has to be written in Objective-C, since Swift Testing supports none of those. Both kinds of test can sit in the same target, so there is no new target to create and no deadline to migrate.

    XCTest vs Swift Testing at a glance

    Area XCTest Swift Testing
    Available in Every current Xcode; swift-corelibs-xctest on Linux and other platforms Xcode 16 and later, Swift 6 toolchains for Apple platforms, Linux and Windows
    Declaring a test A test… method in an XCTestCase subclass @Test on any function; suites can be structs, actors or classes
    Checking a cart total About 40 XCTAssert… functions, XCTUnwrap, XCTFail #expect keeps going after a failure, #require stops the test
    Fresh state per test The setUp and tearDown family, plus teardown blocks init() and deinit on the suite type
    Waiting on a callback XCTestExpectation with wait or fulfillment(of:) await, or confirmation() for event streams
    Grouping and conditions Classes, XCTSkip; test plans shared by both Traits such as .tags, .enabled(if:), .disabled, .bug, .timeLimit, .serialized; nested suites
    Same test, many inputs Loops or copy-pasted methods @Test(arguments:), each input reported as its own case
    Default execution Tests in a suite run one after another In parallel, in-process with Swift Concurrency, including on physical devices
    UI tests with XCUIApplication Yes No, they stay on XCTest
    measure and XCTMetric Yes No, they stay on XCTest
    Objective-C test code Yes Swift only, though it can test code written in other languages
    Mixing in one test Before Xcode 27, avoid calling the other framework's assertions Xcode 27 with Swift 6.4 adds interop modes (none, limited, complete, strict)
    Same test target Yes, side by side Yes, side by side

    The parallel default surprises people most. A Swift Testing suite writing to a shared UserDefaults key can pass alone and fail in a full run, so remove the shared state or add .serialized; our page on parallel testing for mobile apps explains why. Launch-time and scrolling checks stay with XCTest's measure API, and our list of mobile performance testing tools covers what goes beyond it. Seen as Swift Testing vs XCTest for new code, the gain is ergonomic: one #expect(cart.totalInCents == 375) prints both sides of a failed comparison, where XCTest needs a separate assertion per operator.

    How do you migrate XCTest tests to Swift Testing?

    Move one file at a time, starting with unit tests you edit often, and leave UI and performance tests alone. Most of the work is mechanical, following Apple's Migrating a test from XCTest.

    Migrating XCTest to Swift Testing: the common swaps

    XCTest Swift Testing Note
    import XCTest import Testing Import both when one file holds both kinds of test
    class CartTests: XCTestCase struct CartTests (or an actor or class) Pick a class or actor if you need deinit
    func testTotal() @Test func total() No test prefix; a display name is optional
    setUp() / tearDown() init() / deinit init may be async throws
    XCTAssertEqual(x, y) #expect(x == y) The same shape works for !=, >, < and nil checks
    try XCTUnwrap(x) try #require(x) Ends the test when the value is nil
    XCTAssertThrowsError(try f()) #expect(throws: (any Error).self) { try f() } Pass a specific error type to be stricter
    XCTFail("…") Issue.record("…") Records a failure without stopping
    XCTestExpectation plus waiting await, or confirmation() A confirmation must be confirmed before its closure returns
    XCTSkip, XCTSkipIf .enabled(if:), .disabled, Test.cancel Traits skip before the test starts
    XCTExpectFailure withKnownIssue Pass isIntermittent: true for failures that come and go
    XCTAttachment Attachment Attach a JSON payload or log to a failure
    XCTAssertEqual(x, y, accuracy:) No direct equivalent Apple points to isApproximatelyEqual() in swift-numerics

    A practical order of work:

    1. Keep every UI test and measure test in XCTest.
    2. Write all new unit tests in Swift Testing inside the existing target.
    3. Convert the XCTest files your team touches most, a few per sprint.
    4. Add .serialized to suites that share a database, keychain item or singleton.
    5. Add @MainActor to tests that quietly relied on XCTest running synchronous methods on the main thread.

    One rule depends on your Xcode version. Through Xcode 26, Apple's guidance was to keep both frameworks in one target but never call an XCTest assertion from a Swift Testing test, or #expect from an XCTest method, since the failure could go unreported. Xcode 27, which ships Swift 6.4, reports those cross-framework failures and adds an interoperability setting to the test plan, per the Xcode 27 release notes. In Apple's WWDC26 migration session, test plans created before Xcode 27 inherit limited mode (such failures appear as warnings), new projects get complete mode (they stay errors), and strict mode halts the test at each leftover call. For packages, the Swift 6.4 release post covers the change, and the SWIFT_TESTING_XCTEST_INTEROP_MODE environment variable sets a mode explicitly.

    What does an XCTest suite cost to maintain?

    Unit tests in either framework mostly break when the code under test changes, which is their purpose: change Pantry's rounding rule and CartTotalTests should fail. Swift Testing trims boilerplate there, yet the real upkeep bill of an iOS suite sits in the UI layer. Identifiers move with redesigns, waits wrap animations and slow network calls, permission alerts pop up on first launch, and a separate Android suite has to follow every product change. Migrating unit tests does nothing for that work, because UI tests stay on XCTest either way. The flaky XCUITest runs section of our UI guide covers fixes inside Xcode, and our explainer on self-healing test automation covers what changes when a tool repairs broken locators for review.

    Where does QApilot fit next to XCTest?

    QApilot does not run unit tests or replace XCTest or Swift Testing; keep both for logic checks on every pull request, and keep your device cloud. QApilot adds an end-to-end layer on the built IPA, run locally or through a connected provider such as BrowserStack or Sauce Labs.

    On iOS, that layer covers four things:

    • Record and playback. A tester records the Pantry login and checkout journey once on the IPA instead of coding each step.
    • AI auto-healing with a person in the loop. When a step's locator fails, QApilot tries the element ID or accessibility ID first, then XPath and tag attributes, then a visual match, and finally the recorded coordinates. A rescued step is tagged as AI assisted in the report, and the new locator is kept only after someone clicks Update XPath to accept it.
    • Cross-platform test cases. One test case can run against both the IPA and the Android APK, with platform-specific steps where screens or navigation differ, which supports iOS and Android test parity.
    • Test plans and reports. Group cases into plans, trigger them from CI, read step logs and screenshots, and export any plan as Appium code if you want a script copy.

    XCTest and Swift Testing confirm that functions behave; QApilot confirms that the build you are about to ship still lets a person sign in and pay.

    XCTest FAQ

    What is XCTest in simple terms?

    It is the testing toolkit built into Xcode: classes inherit from XCTestCase, each test method exercises one piece of the app, and calls like XCTAssertEqual fail the run when a result is wrong. It also handles timing checks and UI automation.

    What is the quickest way to get XCTest running in a project?

    Add a Unit Testing Bundle target, switch its testing system to XCTest, expose the app module with @testable import, write one method starting with test, and press Command-U. On a build server, xcodebuild test with your scheme gives the same run plus an .xcresult bundle.

    Is XCUITest different from XCTest?

    XCUITest is the UI-automation side of XCTest rather than a rival framework. Unit tests talk to your types directly from inside a unit test bundle, while UI tests live in a separate UI testing bundle and steer the installed app through XCUIApplication.

    Should a team pick XCTest or Swift Testing?

    Pick by test type rather than declaring a winner. Swift Testing suits new unit and integration work on Xcode 16 and newer; XCTest remains the home for UI automation, measure benchmarks and Objective-C tests. Nothing stops you from running both in one target.

    Can one target hold XCTest and Swift Testing tests at once?

    Yes. One target, even one Swift file importing both modules, can hold both styles, so conversion can be gradual. On Xcode 26 and earlier, keep each framework's assertions inside its own tests; Xcode 27 adds test plan interop modes that surface assertions called across the boundary.

    Is Swift Testing replacing XCTest?

    Not entirely. Apple steers new unit tests toward Swift Testing, but UI automation and performance APIs exist only in XCTest, and Xcode 27 added XCUIVoiceOverService for VoiceOver checks, so XCTest is still being developed.

    Related reading

    For the Android side of the same question, read our guide to Espresso testing. To see where frameworks like these sit among commercial options, browse our roundup of automated software testing tools, and for a wider view across platforms, the mobile testing guide for Android, iOS and Flutter.

    Written by

    Harini Mukesh

    Harini Mukesh

    Product Marketing Analyst

    Harini is a Product Marketing Analyst at QApilot with a background in Psychology and Data Analytics. She is interested in understanding user behavior and translating insights into structured, meaningful solutions. She enjoys working at the intersection of data, content, and product thinking, and is particularly curious about how technology and human behavior come together to shape better user experiences.

    Read More...

    Get started

    Start Your Journey to Smarter Mobile App QE

    Rethink how your team approaches mobile testing.

    QApilot - Mobile-First Businesses Need Mobile-First App Testing | Product Hunt
    SOC 2 Type 2 compliance badge
    SOC 2: In Progress
    HIPAA compliance badge
    HIPAA: In Progress
    Copyright © 2026 | Powered by QApilot