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

相关推荐
子兮曰6 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰6 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万6 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝6 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋6 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁6 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95276 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大6 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师6 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学6 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端