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": "[email protected]"
    }
    """.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, "[email protected]")
    } 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

相关推荐
可观测性用观测云13 分钟前
观测云数据在Grafana展示的最佳实践
前端
uwvwko35 分钟前
ctfhow——web入门214~218(时间盲注开始)
前端·数据库·mysql·ctf
Json____35 分钟前
使用vue2开发一个医疗预约挂号平台-前端静态网站项目练习
前端·vue2·网站模板·静态网站·项目练习·挂号系统
littleplayer1 小时前
iOS Swift Redux 架构详解
前端·设计模式·架构
工呈士1 小时前
HTML 模板技术与服务端渲染
前端·html
皮实的芒果1 小时前
前端实时通信方案对比:WebSocket vs SSE vs setInterval 轮询
前端·javascript·性能优化
鹿九巫1 小时前
【CSS】层叠,优先级与继承(三):超详细继承知识点
前端·css
奕云1 小时前
react-redux源码分析
前端
咸鱼一号机1 小时前
:global 是什么
前端
专业掘金1 小时前
0425 手打基础丸
前端