You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在XCTest中为SwiftUI UI测试模拟网络响应与状态?

SwiftUI UI测试中Mock网络请求与登录状态的实现方案

我正在开发一款SwiftUI应用,已经用Swift Testing编写了单元测试,现在需要添加UI测试。这款应用高度依赖网络请求获取数据和执行操作,登录状态对功能可用性至关重要。我已经实现了一个基础UI测试,它会启动应用并调用真实认证服务器完成登录,但测试其他视图时每次都得重复登录步骤。

我知道SwiftUI预览推荐给ViewModel使用协议并创建Mock版本,想知道这种方式是不是XCTest UI测试中模拟网络请求和状态的首选方案?如果是的话,因为UI测试是启动整个应用而非单个视图,该怎么注入这些Mock呢?

let app = XCUIApplication()
app.launch()

应用结构

下面是我的应用简化结构示例。我希望在测试中Mock AuthenticationModel里的isAuthenticated状态,以及Mock网络请求(可在AuthenticationModel或APIClient中实现)。

根据现有建议,我已经引入协议并通过基础依赖注入给AuthenticationModel添加了网络层(APIClient),理论上可以实现网络层Mock,但不清楚如何在XCTest中使用Mock实现。

ContentView.swift

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var authModel: AuthenticationModel
    
    var body: some View {
        if authModel.isAuthenticated {
            VStack {
                Text("Welcome \(authModel.username)")
                
                Button {
                    Task {
                        await authModel.logout()
                    }
                } label: {
                    Text("Logout")
                }
                .buttonStyle(.borderedProminent)
            }
            .padding()
        } else {
            VStack {
                TextField("Username", text: $authModel.username)
                    .multilineTextAlignment(.center)
                TextField("Password", text: $authModel.password)
                    .multilineTextAlignment(.center)
                Button {
                    Task {
                        await authModel.login()
                    }
                } label: {
                    Text("Login")
                }
                .buttonStyle(.borderedProminent)
            }
            .padding()
        }
    }
}

#Preview {
    let authModel = AuthenticationModel(MockAPIClient())
    
    ContentView().environmentObject(authModel)
}

AuthenticationModel.swift

import Foundation

class AuthenticationModel: ObservableObject {
    @Published var isAuthenticated = false
    @Published var username = ""
    @Published var password = ""
    
    init(_ apiClient: APIClient) {
        self.apiClient = apiClient
    }
    
    @MainActor
    func login() async {
        if !username.isEmpty && !password.isEmpty {
            await apiClient.postRequest(
                with: URL(string: "www.example.com/login")!,
                andBody: LoginRequest(username: username, password: password))
            isAuthenticated = true
        }
    }
    
    @MainActor
    func logout() async {
        username = ""
        password = ""
        isAuthenticated = false
    }
}

APIClient.swift

import Foundation

protocol APIClient {
    func getRequest<T: Decodable>(with url: URL) async -> T?
    
    func postRequest<T: Encodable>(with url: URL, andBody body: T) async
}

struct RealAPIClient: APIClient {
    func getRequest<T: Decodable>(with url: URL) async -> T? {
        print("Real Login")
        return nil
    }
    
    func postRequest<T: Encodable>(with url: URL, andBody body: T) async {
        print("Real POST")
    }
}

struct MockAPIClient: APIClient {
    func getRequest<T: Decodable>(with url: URL) async -> T? {
        print("Mock Login")
        return nil
    }
    
    func postRequest<T: Encodable>(with url: URL, andBody body: T) async {
        print("Mock POST")
    }
}

struct LoginRequest: Encodable {
    let username: String
    let password: String
}

MockTestsApp.swift

import SwiftUI

@main
struct MockTestsApp: App {
    @StateObject private var authModel = AuthenticationModel(RealAPIClient())
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(authModel)
        }
    }
}

UI测试

下面是一个基础登录UI测试示例。我希望Mock登录请求,让测试不再依赖外部认证服务器;同时想编写已登录状态的测试,不用重复登录就能测试专属视图。

import XCTest

final class MockTestsUITests: XCTestCase {

    override func setUpWithError() throws {
        continueAfterFailure = false
    }

    override func tearDownWithError() throws {
    }
    
    @MainActor
    func testLogin() throws {
        let app = XCUIApplication()
        app.launch()
        
        app.textFields["Username"].tap()
        app.textFields["Username"].typeText("Example")
        
        app.textFields["Password"].tap()
        app.textFields["Password"].typeText("Password")
        
        app.buttons["Login"].tap()
        
        XCUIApplication().staticTexts["Welcome Example"].tap()
        
        // 这个测试能运行,但我想Mock登录请求。当前实现依赖真实服务器完成登录
    }

    @MainActor
    func testOtherFunctionality() throws {
        // Mock认证状态,这样测试不用执行登录步骤
        // 测试其他功能
    }

    @MainActor
    func testLaunchPerformance() throws {
        if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
            measure(metrics: [XCTApplicationLaunchMetric()]) {
                XCUIApplication().launch()
            }
        }
    }
}

解决方案

1. 通过启动参数区分测试环境

在UI测试启动应用时传递自定义启动参数,让应用在启动时判断是否使用Mock依赖:

修改MockTestsApp.swift,根据启动参数选择APIClient:

import SwiftUI

@main
struct MockTestsApp: App {
    @StateObject private var authModel: AuthenticationModel
    
    init() {
        let arguments = ProcessInfo.processInfo.arguments
        let useMock = arguments.contains("-ui-test-mock")
        let apiClient: APIClient = useMock ? MockAPIClient() : RealAPIClient()
        _authModel = StateObject(wrappedValue: AuthenticationModel(apiClient))
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(authModel)
        }
    }
}

在UI测试中启动应用时添加参数:

// 测试方法内
let app = XCUIApplication()
app.launchArguments.append("-ui-test-mock")
app.launch()

2. 扩展Mock实现,直接模拟登录状态

修改AuthenticationModel,添加可选的初始认证状态参数:

class AuthenticationModel: ObservableObject {
    @Published var isAuthenticated = false
    @Published var username = ""
    @Published var password = ""
    
    private let apiClient: APIClient
    
    init(_ apiClient: APIClient, initialAuthenticated: Bool = false, initialUsername: String = "") {
        self.apiClient = apiClient
        self.isAuthenticated = initialAuthenticated
        self.username = initialUsername
    }
    
    // 原有方法保持不变
}

更新MockTestsApp.swift的初始化逻辑,根据启动参数传递初始状态:

init() {
    let arguments = ProcessInfo.processInfo.arguments
    let useMock = arguments.contains("-ui-test-mock")
    let isPreAuthenticated = arguments.contains("-ui-test-pre-authenticated")
    let testUsername = arguments.first(where: { $0.starts(with: "-ui-test-username=") })?.replacingOccurrences(of: "-ui-test-username=", with: "") ?? "TestUser"
    
    let apiClient: APIClient = useMock ? MockAPIClient() : RealAPIClient()
    _authModel = StateObject(wrappedValue: AuthenticationModel(apiClient, initialAuthenticated: isPreAuthenticated, initialUsername: testUsername))
}

在UI测试中直接测试已登录状态:

@MainActor
func testOtherFunctionality() throws {
    let app = XCUIApplication()
    app.launchArguments.append("-ui-test-mock")
    app.launchArguments.append("-ui-test-pre-authenticated")
    app.launchArguments.append("-ui-test-username=Example")
    app.launch()
    
    // 验证已登录视图存在
    XCTAssertTrue(app.staticTexts["Welcome Example"].exists)
    
    // 测试登出功能
    app.buttons["Logout"].tap()
    XCTAssertTrue(app.textFields["Username"].exists)
}

3. 自定义MockAPIClient的行为

如果需要模拟登录成功/失败等不同场景,可给MockAPIClient添加配置项:

struct MockAPIClient: APIClient {
    var shouldSucceedLogin = true
    
    func getRequest<T: Decodable>(with url: URL) async -> T? {
        print("Mock Login")
        return nil
    }
    
    func postRequest<T: Encodable>(with url: URL, andBody body: T) async {
        print("Mock POST")
        // 可根据URL判断是否为登录请求,结合配置模拟结果
        if url.absoluteString.contains("login"), !shouldSucceedLogin {
            // 模拟登录失败逻辑,比如可以在ViewModel中添加错误状态,这里配合启动参数控制
        }
    }
}

再通过启动参数传递配置,比如-ui-test-login-failure,在MockTestsApp初始化时设置shouldSucceedLogin为false即可。


内容的提问来源于stack exchange,提问作者Pieter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 07:05:54