iOS 单元测试与 UI 测试详解-DeepSeek

iOS 单元测试与 UI 测试详解

在 iOS 开发中,测试主要分为单元测试(Unit Test)和 UI 测试(UI Test),下面我将详细介绍如何实施这两种测试。

一、单元测试 (Unit Test)

1. 单元测试核心概念

单元测试是针对代码中最小可测试单元的测试,通常是测试单个函数或方法。

2. 如何编写单元测试

基础测试示例

swift

scss 复制代码
import XCTest
@testable import YourApp

class MathTests: XCTestCase {
    
    func testAddition() {
        let result = Calculator().add(2, 3)
        XCTAssertEqual(result, 5, "2 + 3 应该等于 5")
    }
    
    func testDivision() {
        let calculator = Calculator()
        XCTAssertEqual(calculator.divide(10, by: 2), 5)
        XCTAssertThrowsError(try calculator.divide(10, by: 0))
    }
}
测试模型层

swift

csharp 复制代码
func testUserModel() {
    // 准备测试数据
    let userJSON = """
    {
        "id": 123,
        "name": "John Doe",
        "email": "john@example.com"
    }
    """.data(using: .utf8)!
    
    // 测试解码
    do {
        let user = try JSONDecoder().decode(User.self, from: userJSON)
        XCTAssertEqual(user.id, 123)
        XCTAssertEqual(user.name, "John Doe")
        XCTAssertEqual(user.email, "john@example.com")
    } catch {
        XCTFail("解码失败: (error)")
    }
}
测试网络请求

swift

php 复制代码
func testNetworkRequest() {
    // 1. 创建模拟网络环境
    let config = URLSessionConfiguration.ephemeral
    config.protocolClasses = [MockURLProtocol.self]
    let session = URLSession(configuration: config)
    
    // 2. 准备模拟响应数据
    let testData = """
    {"status": "success", "data": {"id": 1, "name": "Test"}}
    """.data(using: .utf8)!
    
    MockURLProtocol.requestHandler = { request in
        let response = HTTPURLResponse(url: request.url!, 
                                      statusCode: 200, 
                                      httpVersion: nil, 
                                      headerFields: nil)!
        return (response, testData)
    }
    
    // 3. 创建期望
    let expectation = XCTestExpectation(description: "Network request")
    
    // 4. 执行测试
    let networkManager = NetworkManager(session: session)
    networkManager.fetchUser(id: 1) { result in
        switch result {
        case .success(let user):
            XCTAssertEqual(user.id, 1)
            XCTAssertEqual(user.name, "Test")
        case .failure(let error):
            XCTFail("请求失败: (error)")
        }
        expectation.fulfill()
    }
    
    // 5. 等待期望完成
    wait(for: [expectation], timeout: 1)
}

3. 测试 ViewModel/Presenter

swift

scss 复制代码
func testLoginViewModel() {
    let mockService = MockAuthService()
    let viewModel = LoginViewModel(authService: mockService)
    
    // 测试初始状态
    XCTAssertFalse(viewModel.isLoading)
    XCTAssertFalse(viewModel.isLoggedIn)
    
    // 模拟成功登录
    mockService.loginResult = .success(true)
    viewModel.login(username: "test", password: "123456")
    
    // 验证状态变化
    XCTAssertTrue(viewModel.isLoading)
    
    // 使用期望等待异步操作
    let expectation = XCTestExpectation(description: "Login completed")
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        XCTAssertFalse(viewModel.isLoading)
        XCTAssertTrue(viewModel.isLoggedIn)
        expectation.fulfill()
    }
    
    wait(for: [expectation], timeout: 1)
}

二、UI 测试 (UI Test)

1. UI 测试核心概念

UI 测试模拟用户与应用程序的交互,验证整个用户界面的行为是否符合预期。

2. 如何编写 UI 测试

基础 UI 测试示例

swift

scss 复制代码
import XCTest

class LoginUITests: XCTestCase {
    
    var app: XCUIApplication!
    
    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()
    }
    
    func testLoginFlow() {
        // 定位界面元素
        let usernameField = app.textFields["username"]
        let passwordField = app.secureTextFields["password"]
        let loginButton = app.buttons["loginButton"]
        
        // 验证元素存在
        XCTAssertTrue(usernameField.exists)
        XCTAssertTrue(passwordField.exists)
        XCTAssertTrue(loginButton.exists)
        
        // 输入测试数据
        usernameField.tap()
        usernameField.typeText("testuser")
        
        passwordField.tap()
        passwordField.typeText("password123")
        
        // 执行登录操作
        loginButton.tap()
        
        // 验证登录结果
        let welcomeText = app.staticTexts["Welcome, testuser"]
        XCTAssertTrue(welcomeText.waitForExistence(timeout: 5))
    }
    
    func testInvalidLogin() {
        let usernameField = app.textFields["username"]
        let passwordField = app.secureTextFields["password"]
        let loginButton = app.buttons["loginButton"]
        
        // 输入无效凭据
        usernameField.tap()
        usernameField.typeText("wrong")
        
        passwordField.tap()
        passwordField.typeText("wrong")
        
        loginButton.tap()
        
        // 验证错误提示
        let alert = app.alerts["Login Failed"]
        XCTAssertTrue(alert.waitForExistence(timeout: 2))
        XCTAssertTrue(alert.staticTexts["Invalid username or password"].exists)
    }
}
测试 TableView/CollectionView

swift

scss 复制代码
func testTableView() {
    // 确保表格存在
    let tableView = app.tables["itemTableView"]
    XCTAssertTrue(tableView.exists)
    
    // 获取单元格数量
    let cells = tableView.cells
    XCTAssertGreaterThan(cells.count, 0)
    
    // 测试第一个单元格
    let firstCell = cells.element(boundBy: 0)
    XCTAssertTrue(firstCell.exists)
    
    // 点击第一个单元格
    firstCell.tap()
    
    // 验证详情页显示
    let detailTitle = app.staticTexts["detailTitle"]
    XCTAssertTrue(detailTitle.waitForExistence(timeout: 1))
}
测试复杂手势

swift

less 复制代码
func testSwipeToDelete() {
    let tableView = app.tables["itemTableView"]
    let initialCount = tableView.cells.count
    
    // 获取第一个单元格
    let firstCell = tableView.cells.element(boundBy: 0)
    
    // 向左滑动
    let start = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5))
    let end = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.5))
    start.press(forDuration: 0.1, thenDragTo: end)
    
    // 点击删除按钮
    let deleteButton = app.buttons["Delete"]
    XCTAssertTrue(deleteButton.waitForExistence(timeout: 1))
    deleteButton.tap()
    
    // 验证单元格数量减少
    let newCount = tableView.cells.count
    XCTAssertEqual(newCount, initialCount - 1)
}

3. UI 测试最佳实践

  1. 使用 accessibility identifiers 而不是文本内容定位元素

    swift

    less 复制代码
    // 在代码中设置
    button.accessibilityIdentifier = "loginButton"
    
    // 在测试中引用
    app.buttons["loginButton"]
  2. 等待元素出现 而不是硬编码 sleep

    swift

    less 复制代码
    let element = app.staticTexts["welcomeText"]
    XCTAssertTrue(element.waitForExistence(timeout: 5))
  3. 重置应用状态 在每个测试前

    swift

    scss 复制代码
    override func setUp() {
        super.setUp()
        app.launchArguments.append("--uitesting")
        app.launch()
    }
  4. 处理系统弹窗 (如位置权限)

    swift

    javascript 复制代码
    addUIInterruptionMonitor(withDescription: "Location Permission") { (alert) -> Bool in
        if alert.buttons["Allow"].exists {
            alert.buttons["Allow"].tap()
            return true
        }
        return false
    }

三、单元测试与 UI 测试对比

特性 单元测试 UI 测试
测试范围 单个函数/方法 完整用户流程
执行速度
维护成本
稳定性 较低
适合场景 业务逻辑、算法 用户交互、界面流程
依赖 最小化依赖,使用mock 完整应用环境

四、测试金字塔实践建议

  1. 大量单元测试 (70%) - 测试核心业务逻辑
  2. 适量集成测试 (20%) - 测试模块间交互
  3. 少量UI测试 (10%) - 测试关键用户流程

五、常见问题解决

单元测试问题

问题1 : @testable import 找不到模块

  • 解决方案:

    1. 确保测试目标的"Enable Testability"设置为Yes
    2. 检查scheme设置是否正确

问题2: 异步测试不稳定

  • 解决方案:

    swift

    scss 复制代码
    let expectation = XCTestExpectation(description: "Async test")
    someAsyncCall {
        expectation.fulfill()
    }
    wait(for: [expectation], timeout: 5)

UI 测试问题

问题1: 元素找不到

  • 解决方案:

    1. 使用accessibilityIdentifier而不是文本
    2. 增加等待时间 waitForExistence(timeout:)

问题2: 测试在模拟器上运行慢

  • 解决方案:

    1. 关闭动画 app.launchArguments += ["-UIAnimationSpeed", "2"]
    2. 使用更快的模拟器设备

通过合理组合单元测试和UI测试,可以构建全面的iOS应用测试体系,有效提高应用质量和开发效率。

from: deepseek

相关推荐
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte6 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc