Network-backed UI paths need deterministic data before they can become useful XCUITest coverage. A mock boundary inside the app keeps login, profile, checkout, or empty-state screens testable when the real API is slow, unavailable, rate-limited, or unsafe for repeat test runs.
The UI test process cannot intercept URLSession traffic in the app process by itself. Pass a launch environment value with XCUIApplication, let the app read that value during startup, and configure the app's network client to use a test-only URLProtocol fixture for the API host.
Keep the mock path narrow enough that an unmocked endpoint fails visibly instead of falling through to production. The test should pass only when the app renders data from the fixture response and writes its result bundle for later failure review.
Related: How to use launch arguments in XCUITest
Related: How to run XCUITest with xcodebuild in CI
Related: How to save XCUITest result bundles
Mock trigger: UITEST_NETWORK_MOCKS=enabled Mocked host: api.example.com Fixture path: /profile Expected UI state: profile-name shows Fixture User
Use a host and path owned by the app's API client. Do not point the production base URL at a local test server unless that is already the app's supported test mode.
import Foundation struct Profile: Decodable { let name: String let plan: String } final class FixtureURLProtocol: URLProtocol { override class func canInit(with request: URLRequest) -> Bool { request.url?.host == "api.example.com" } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { let path = request.url?.path ?? "/" let statusCode: Int let body: Data switch path { case "/profile": statusCode = 200 body = Data(#"{"name":"Fixture User","plan":"Test"}"#.utf8) default: statusCode = 500 body = Data(#"{"error":"missing fixture"}"#.utf8) } let response = HTTPURLResponse( url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"] )! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: body) client?.urlProtocolDidFinishLoading(self) } override func stopLoading() {} }
The class belongs in the app target because the app process owns the network session. The fallback response makes an unexpected API path fail instead of reaching the real service.
import Foundation struct APIClient { let session: URLSession static func make() -> APIClient { let configuration = URLSessionConfiguration.default if ProcessInfo.processInfo.environment["UITEST_NETWORK_MOCKS"] == "enabled" { configuration.protocolClasses = [FixtureURLProtocol.self] } return APIClient(session: URLSession(configuration: configuration)) } func loadProfile() async throws -> Profile { let url = URL(string: "https://api.example.com/profile")! let (data, _) = try await session.data(from: url) return try JSONDecoder().decode(Profile.self, from: data) } }
Keep the fixture branch behind a test-only environment value. Shipping code should create the normal URLSession configuration when UITEST_NETWORK_MOCKS is absent.
import XCTest final class ProfileNetworkMockUITests: XCTestCase { private var app: XCUIApplication! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launchEnvironment["UITEST_NETWORK_MOCKS"] = "enabled" app.launch() } func testProfileScreenUsesFixtureResponse() throws { app.buttons["profile-refresh"].tap() let profileName = app.staticTexts["profile-name"] XCTAssertTrue( profileName.waitForExistence(timeout: 5), "Profile name did not appear" ) XCTAssertEqual(profileName.label, "Fixture User") } }
The app must expose stable accessibility identifiers for the refresh control and rendered profile label.
Related: How to wait for UI elements in XCUITest
$ mkdir -p TestResults
$ xcodebuild test \ -workspace MyApp.xcworkspace \ -scheme MyApp \ -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ -only-testing:MyAppUITests/ProfileNetworkMockUITests \ -resultBundlePath TestResults/NetworkMocks.xcresult Test Suite 'Selected tests' started. Test Case '-[MyAppUITests.ProfileNetworkMockUITests testProfileScreenUsesFixtureResponse]' started. Test Case '-[MyAppUITests.ProfileNetworkMockUITests testProfileScreenUsesFixtureResponse]' passed (4.018 seconds). ** TEST SUCCEEDED **
Use -project MyApp.xcodeproj instead of -workspace MyApp.xcworkspace when the app does not use a workspace.
Related: How to run XCUITest with xcodebuild in CI
$ ls -d TestResults/NetworkMocks.xcresult TestResults/NetworkMocks.xcresult
Open the bundle with open TestResults/NetworkMocks.xcresult when a failure needs screenshots, logs, or the selected test report.
Related: How to save XCUITest result bundles