UnitTesting 单元测试

1. 测试分为两种及详细介绍测试书籍:

1.1 Unit Test : 单元测试

  • test the business logic in your app : 测试应用中的业务逻辑

1.2 UI Test : 界面测试

  • test the UI of your app : 测试应用中的界面

1.3 测试书籍网址:《Testing Swift》 https://www.hackingwithswift.com/store/testing-swift

2. ViewModel 单元测试

2.1 创建 ViewModel,UnitTestingBootcampViewModel.swift

Swift 复制代码
import Foundation
import SwiftUI
import Combine

/// 单元测试 ViewModel
class UnitTestingBootcampViewModel: ObservableObject{
    @Published var isPremium: Bool
    @Published var dataArray: [String] = []
    @Published var selectedItem: String? = nil
    let dataService: NewDataServiceProtocol
    var cancellable = Set<AnyCancellable>()
    
    init(isPremium: Bool, dataService: NewDataServiceProtocol = NewMockDataService(items: nil)) {
        self.isPremium = isPremium
        self.dataService = dataService
    }
    
    /// 添加子项
    func addItem(item: String){
        // 为空不往下执行
        guard !item.isEmpty else { return }
        self.dataArray.append(item)
    }
    
    /// 选中项
    func selectItem(item: String){
        if let x = dataArray.first(where: {$0 == item}){
            selectedItem = x
        }else{
            selectedItem = nil
        }
    }
    
    /// 保存项
    func saveItem(item: String) throws{
        guard !item.isEmpty else{
            throw DataError.noData
        }
        
        if let x = dataArray.first(where: {$0 == item}){
            print("Save item here!!! \(x)")
        } else {
            throw DataError.itemNotFound
        }
    }
    
    /// 错误信息
    enum DataError: LocalizedError{
        case noData
        case itemNotFound
    }
    
    /// 请求返回数据
    func downloadWithEscaping() {
        dataService.downloadItemsWithEscaping { [weak self] returnedItems in
            self?.dataArray = returnedItems
        }
    }
    
    /// 下载用到的组合
    func downloadWithCombine() {
        dataService.downloadItemsWithCombine()
            .sink { _ in
                
            } receiveValue: { [weak self] returnedItems in
                self?.dataArray = returnedItems
            }
            .store(in: &cancellable)
    }
}

2.2 创建测试文件

当创建项目时,没有选择 Include Tests/包含测试 选项时,需要添加文件去对应项目,不然测试文件会报 No such module 'XCTest' 编译错误

添加单元测试文件:

方法一 : 选择项目 -> 菜单栏 Editor -> Add Target... -> 弹出对话框,选择 Test 栏下 -> Unit Testing Bundle -> 填写信息/可默认 -> Finish,完成创建单元测试文件。

方法二 : 选择项目,点击 PROJECT 列,最下的 + 按钮,弹出对话框,选择 Test 栏下 ,后面步骤与上一致

创建单元测试文件 UnitTestingBootcampViewModel_Tests.swift

Swift 复制代码
import XCTest
import Combine
/// 导入项目
@testable import SwiftfulThinkingAdvancedLearning

// 《Testing Swift》 测试书籍
// 书籍网址: https://www.hackingwithswift.com/store/testing-swift
// Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior -  结构体命名: 测试_工作单元_测试状态_预期的行为
// Naming Structure: test_[struct or class]_[variable or function]_[expected result] - 测试_[结构体 或者 类的名称]_[类中的变量名 或者 函数名称]_[预期结果 预期值]
// Testing Structure: Given, When, Then - 测试结构: 给定,什么时候,然后

final class UnitTestingBootcampViewModel_Tests: XCTestCase {
    /// 解决多次引用相同的类
    var viewModel: UnitTestingBootcampViewModel?
    var cancellables = Set<AnyCancellable>()
    
    /// 开始设置数据
    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
        viewModel = UnitTestingBootcampViewModel(isPremium: Bool.random())
    }
    
    /// 结束重置数据
    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        viewModel = nil
        cancellables.removeAll()
    }
    
    /// 单元测试函数名,根据命名规则命名:测试_类名称_是否高质量_应该为真
    func test_UnitTestingBootcampViewModel_isPremium_shouldBeTrue(){
        // Given
        let userIsPremium: Bool = true
        
        // When
        let vm = UnitTestingBootcampViewModel(isPremium: userIsPremium)
        
        // Then
        XCTAssertTrue(vm.isPremium)
    }
    
    /// 单元测试函数名 根据命名规则命名:测试_类名称_是否高质量_应该为假
    func test_UnitTestingBootcampViewModel_isPremium_shouldBeFalse(){
        // Given
        let userIsPremium: Bool = false
        
        // When
        let vm = UnitTestingBootcampViewModel(isPremium: userIsPremium)
        
        // Then
        XCTAssertFalse(vm.isPremium)
    }
    
    /// 单元测试函数名 根据命名规则命名:测试_类名称_是否高品质_注入值
    func test_UnitTestingBootcampViewModel_isPremium_shouldBeInjectedValue(){
        // Given
        let userIsPremium: Bool = Bool.random()
        
        // When
        let vm = UnitTestingBootcampViewModel(isPremium: userIsPremium)
        
        // Then
        XCTAssertEqual(vm.isPremium, userIsPremium)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 注入值_压力 / for 循环
    func test_UnitTestingBootcampViewModel_isPremium_shouldBeInjectedValue_stress(){
        for _ in 0 ..< 10 {
            // Given
            let userIsPremium: Bool = Bool.random()
            // When
            let vm = UnitTestingBootcampViewModel(isPremium: userIsPremium)
            // Then
            XCTAssertEqual(vm.isPremium, userIsPremium)
        }
    }
    
    /// 单元测试函数名 根据命名规则命名 - 数组_预期值:为空
    func test_UnitTestingBootcampViewModel_dataArray_shouldBeEmpty(){
        // Given
        
        // When
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // Then 断言 = 判定
        XCTAssertTrue(vm.dataArray.isEmpty)
        XCTAssertEqual(vm.dataArray.count, 0)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 数组_预期值:添加项
    func test_UnitTestingBootcampViewModel_dataArray_shouldAddItems(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // When
        let loopCount: Int = Int.random(in: 1..<100)
        
        for _ in 0 ..< loopCount{
            vm.addItem(item: UUID().uuidString)
        }
        
        // Then 断言 = 判定
        XCTAssertTrue(!vm.dataArray.isEmpty)
        XCTAssertFalse(vm.dataArray.isEmpty)
        XCTAssertEqual(vm.dataArray.count, loopCount)
        XCTAssertNotEqual(vm.dataArray.count, 0)
        // GreaterThan 大于
        XCTAssertGreaterThan(vm.dataArray.count, 0)
        // XCTAssertGreaterThanOrEqual
        // XCTAssertLessThan
        // XCTAssertLessThanOrEqual
    }
    
    /// 单元测试函数名 根据命名规则命名 - 数组_预期值:添加空白字符
    func test_UnitTestingBootcampViewModel_dataArray_shouldNotAddBlankString(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // When
        vm.addItem(item: "")
        
        // Then 断言 = 判定
        XCTAssertTrue(vm.dataArray.isEmpty)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 数组_预期值:添加空白字符
    func test_UnitTestingBootcampViewModel_dataArray_shouldNotAddBlankString2(){
        // Given
        guard let vm = viewModel else {
            XCTFail()
            return
        }
        
        // When
        vm.addItem(item: "")
        
        // Then 断言 = 判定
        XCTAssertTrue(vm.dataArray.isEmpty)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 选中项_预期值:开始为空
    func test_UnitTestingBootcampViewModel_selectedItem_shouldStartAsNil(){
        // Given
        
        // When
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // Then 断言 = 判定
        XCTAssertTrue(vm.selectedItem == nil)
        XCTAssertNil(vm.selectedItem)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 选中项_预期值:应该为空 当选择无效项
    func test_UnitTestingBootcampViewModel_selectedItem_shouldBeNilWhenSelectingInvalidItem(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // Select valid item : 选择有效项
        let newItem = UUID().uuidString
        vm.addItem(item: newItem)
        vm.selectItem(item: newItem)
        
        // Select invalid item : 选择无效项
        // When
        vm.selectItem(item: UUID().uuidString)
        
        // Then 断言 = 判定
        XCTAssertNil(vm.selectedItem)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 选中项_预期值:应该选中
    func test_UnitTestingBootcampViewModel_selectedItem_shouldBeSelected(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // When
        let newItem = UUID().uuidString
        vm.addItem(item: newItem)
        vm.selectItem(item: newItem)
        
        // Then 断言 = 判定
        XCTAssertNotNil(vm.selectedItem)
        XCTAssertEqual(vm.selectedItem, newItem)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 选中项_预期值:选中_压力测试
    func test_UnitTestingBootcampViewModel_selectedItem_shouldBeSelected_stress(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // When
        let loopCount: Int = Int.random(in: 1..<100)
        var itemsArray: [String] = []
        
        for _ in 0 ..< loopCount {
            let newItem = UUID().uuidString
            vm.addItem(item: newItem)
            itemsArray.append(newItem)
        }
        
        // 随机取一个字符串
        let randomItem = itemsArray.randomElement() ?? ""
        // 检查字符串不为空
        XCTAssertFalse(randomItem.isEmpty)
        vm.selectItem(item: randomItem)
        
        // Then 断言 = 判定
        XCTAssertNotNil(vm.selectedItem)
        XCTAssertEqual(vm.selectedItem, randomItem)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 保存项_预期值:输出错误异常_元素没找到
    func test_UnitTestingBootcampViewModel_saveItem_shouldThrowError_itemNotFound(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // When
        let loopCount: Int = Int.random(in: 1..<100)
        for _ in 0 ..< loopCount {
            vm.addItem(item: UUID().uuidString)
        }
        
        // Then 断言 = 判定
        XCTAssertThrowsError(try vm.saveItem(item: UUID().uuidString))
        XCTAssertThrowsError(try vm.saveItem(item: UUID().uuidString), "Should throw Item Not Found error!") { error in
            // 返回错误
            let returnedError = error as? UnitTestingBootcampViewModel.DataError
            // 判断错误是否相同
            XCTAssertEqual(returnedError, UnitTestingBootcampViewModel.DataError.itemNotFound)
        }
    }
    
    /// 单元测试函数名 根据命名规则命名 - 保存项_预期值:输出错误异常_没数据
    func test_UnitTestingBootcampViewModel_saveItem_shouldThrowError_noData(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // When
        let loopCount: Int = Int.random(in: 1..<100)
        for _ in 0 ..< loopCount {
            vm.addItem(item: UUID().uuidString)
        }
        
        // Then 断言 = 判定
        do {
            try vm.saveItem(item: "")
        } catch let error {
            // 返回错误
            let returnedError = error as? UnitTestingBootcampViewModel.DataError
            // 判断错误是否相同
            XCTAssertEqual(returnedError, UnitTestingBootcampViewModel.DataError.noData)
        }
    }
    
    /// 单元测试函数名 根据命名规则命名 - 保存项_预期值:保存选项
    func test_UnitTestingBootcampViewModel_saveItem_shouldSaveItem(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // When
        let loopCount: Int = Int.random(in: 1..<100)
        var itemsArray: [String] = []
        
        for _ in 0 ..< loopCount {
            let newItem = UUID().uuidString
            vm.addItem(item: newItem)
            itemsArray.append(newItem)
        }
        
        // 随机取一个字符串
        let randomItem = itemsArray.randomElement() ?? ""
        // 检查字符串不为空
        XCTAssertFalse(randomItem.isEmpty)
        // Then 断言 = 判定
        XCTAssertNoThrow(try vm.saveItem(item: randomItem))
        do {
            try vm.saveItem(item: randomItem)
        } catch  {
            XCTFail()
        }
    }
    
    /// 单元测试函数名 根据命名规则命名 - 下载数据_预期值:返回选项
    func test_UnitTestingBootcampViewModel_downloadWithEscaping_shouldReturnItems(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // When
        let expectation = XCTestExpectation(description: "Should return items after 3 seconds")
        // dropFirst: 删除第一个发布 数组值,因为初始化为空数组,取的是第二个数组,模拟服务数据返回的数组
        vm.$dataArray
            .dropFirst()
            .sink { returnedItems in
                expectation.fulfill()
            }
            .store(in: &cancellables)
        
        vm.downloadWithEscaping()
        
        // Then 断言 = 判定 GreaterThan:大于
        // 为了安全获取到值,设置等待 5 秒
        wait(for: [expectation], timeout: 5)
        XCTAssertGreaterThan(vm.dataArray.count, 0)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 下载数据组合_预期值:返回选项
    func test_UnitTestingBootcampViewModel_downloadWithCombine_shouldReturnItems(){
        // Given
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
        
        // When
        let expectation = XCTestExpectation(description: "Should return items after a seconds")
        // dropFirst: 删除第一个发布 数组值,因为初始化为空数组,取的是第二个数组,模拟服务数据返回的数组
        vm.$dataArray
            .dropFirst()
            .sink { returnedItems in
                expectation.fulfill()
            }
            .store(in: &cancellables)
        
        vm.downloadWithCombine()
        
        // Then 断言 = 判定 GreaterThan:大于
        // 为了安全获取到值,设置等待 5 秒
        wait(for: [expectation], timeout: 5)
        XCTAssertGreaterThan(vm.dataArray.count, 0)
    }
    
    /// 单元测试函数名 根据命名规则命名 - 下载数据组合_预期值:返回选项
    func test_UnitTestingBootcampViewModel_downloadWithCombine_shouldReturnItems2(){
        // Given
        let items: [String] = [UUID().uuidString, UUID().uuidString, UUID().uuidString, UUID().uuidString]
        let dataService: NewDataServiceProtocol = NewMockDataService(items: items)
        let vm = UnitTestingBootcampViewModel(isPremium: Bool.random(), dataService: dataService)
        
        // When
        let expectation = XCTestExpectation(description: "Should return items after a seconds")
        // dropFirst: 删除第一个发布 数组值,因为初始化为空数组,取的是第二个数组,模拟服务数据返回的数组
        vm.$dataArray
            .dropFirst()
            .sink { returnedItems in
                expectation.fulfill()
            }
            .store(in: &cancellables)
        
        vm.downloadWithCombine()
        
        // Then 断言 = 判定 GreaterThan:大于
        // 为了安全获取到值,设置等待 5 秒
        wait(for: [expectation], timeout: 5)
        XCTAssertGreaterThan(vm.dataArray.count, 0)
        XCTAssertEqual(vm.dataArray.count, items.count)
    }
}

3. 模拟请求数据 单元测试

3.1 创建模拟请求数据类 NewMockDataService.swift

Swift 复制代码
import Foundation
import SwiftUI
import Combine

/// 定义协议
protocol NewDataServiceProtocol{
    func downloadItemsWithEscaping(completion: @escaping (_ items: [String]) -> ())
    func downloadItemsWithCombine() -> AnyPublisher<[String], Error>
}

/// 实现模拟请求数据
class NewMockDataService: NewDataServiceProtocol {
    let items: [String]
    
    init(items: [String]?) {
        self.items = items ?? [
            "ONE", "TWO", "THREE"
        ]
    }
    
    /// 模拟网络下载数据 escaping: 转义字符
    func downloadItemsWithEscaping(completion: @escaping (_ items: [String]) -> ()) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            completion(self.items)
        }
    }
    
    /// 下载组合
    func downloadItemsWithCombine() -> AnyPublisher<[String], Error> {
        // 数据转换
        Just(self.items)
            .tryMap({ publishedItems in
                guard !publishedItems.isEmpty else {
                    throw URLError(.badServerResponse)
                }
                return publishedItems
            })
            .eraseToAnyPublisher()
    }
}

3.2 创建单元测试类 NewMockDataService_Tests.swift

Swift 复制代码
import XCTest
import Combine
/// 导入项目
@testable import SwiftfulThinkingAdvancedLearning

final class NewMockDataService_Tests: XCTestCase {
    /// 随时取消控制器
    var cancellable = Set<AnyCancellable>()
    
    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        cancellable.removeAll()
    }
    
    //  单元测试函数名 根据命名规则命名 - 测试_类名_初始化_预期值:正确的设置值
    func test_NewMockDataService_init_doesSetValuesCorrectly() {
        // 执行
        // Given: 给定
        let items: [String]? = nil
        let items2: [String]? = []
        let items3: [String]? = [UUID().uuidString, UUID().uuidString]
        
        // When: 时间
        let dataService = NewMockDataService(items: items)
        let dataService2 = NewMockDataService(items: items2)
        let dataService3 = NewMockDataService(items: items3)
        
        // Then 然后
        XCTAssertFalse(dataService.items.isEmpty)
        XCTAssertTrue(dataService2.items.isEmpty)
        XCTAssertEqual(dataService3.items.count, items3?.count)
    }
    
    //  单元测试函数名 根据命名规则命名 - 测试_类名_下载转换数据项_预期值:正确的设置值
    func test_NewMockDataService_downloadItemsWithEscaping_doesReturnValues() {
        // 执行
        // Given: 给定
        let dataService = NewMockDataService(items: nil)
        
        // When: 时间
        var items: [String] = []
        let expectation = XCTestExpectation()
        dataService.downloadItemsWithEscaping { returnedItems in
            items = returnedItems
            expectation.fulfill()
        }
        
        // Then 然后
        // 等待 5 秒
        wait(for: [expectation], timeout: 5)
        // 断言两个数组大小一样
        XCTAssertEqual(items.count, dataService.items.count)
    }
    
    //  单元测试函数名 根据命名规则命名 - 测试_类名_下载数据项组合_预期值:正确的设置值
    func test_NewMockDataService_downloadItemsWithCombine_doesReturnValues() {
        // 执行
        // Given: 给定
        let dataService = NewMockDataService(items: nil)
        
        // When: 时间
        var items: [String] = []
        let expectation = XCTestExpectation()
        
        // 下载组合控制
        dataService.downloadItemsWithCombine()
            .sink { completion in
                switch completion{
                case .finished:
                    expectation.fulfill()
                case .failure:
                    XCTFail()
                }
            } receiveValue: {returnedItems in
                // fulfill: 完成
                items = returnedItems
            }
            .store(in: &cancellable)
        // Then 然后
        // 等待 5 秒
        wait(for: [expectation], timeout: 5)
        // 断言两个数组大小一样
        XCTAssertEqual(items.count, dataService.items.count)
    }
    
    //  单元测试函数名 根据命名规则命名 - 测试_类名_下载数据项组合_预期值:确实失败
    func test_NewMockDataService_downloadItemsWithCombine_doesFail() {
        // 执行
        // Given: 给定
        let dataService = NewMockDataService(items: [])
        
        // When: 时间
        var items: [String] = []
        let expectation = XCTestExpectation(description: "Does throw an error")
        let expectation2 = XCTestExpectation(description: "Does throw URLError.badServerResponse")
        // 下载组合控制
        dataService.downloadItemsWithCombine()
            .sink { completion in
                switch completion{
                case .finished:
                    XCTFail()
                case .failure(let error):
                    expectation.fulfill()
                    
                    //let urlError = error as? URLError
                    // 断言,判定
                    //XCTAssertEqual(urlError, URLError(.badServerResponse))
                    
                    // 错误判断
                    if error as? URLError == URLError(.badServerResponse) {
                        expectation2.fulfill()
                    }
                }
            } receiveValue: {returnedItems in
                // fulfill: 完成
                items = returnedItems
            }
            .store(in: &cancellable)
        // Then 然后
        // 等待 5 秒
        wait(for: [expectation, expectation2], timeout: 5)
        // 断言两个数组大小一样
        XCTAssertEqual(items.count, dataService.items.count)
    }
}

4. 创建单元测试 View,调用测试的 ViewModel UnitTestingBootcampView.swift

Swift 复制代码
import SwiftUI

/*
 1. Unit Test : 单元测试
 - test the business logic in your app : 测试应用中的业务逻辑
 
 2. UI  Test :  界面测试
 - test the UI of your app : 测试应用中的界面
 */

/// 单元测试
struct UnitTestingBootcampView: View {
    @StateObject private var vm: UnitTestingBootcampViewModel
    
    init(isPremium: Bool){
        _vm = StateObject(wrappedValue: UnitTestingBootcampViewModel(isPremium: isPremium))
    }
    
    var body: some View {
        Text(vm.isPremium.description)
    }
}

struct UnitTestingBootcampView_Previews: PreviewProvider {
    static var previews: some View {
        UnitTestingBootcampView(isPremium: true)
    }
}
相关推荐
I烟雨云渊T5 小时前
iOS 门店营收表格功能的实现
ios
明月看潮生11 小时前
青少年编程与数学 01-011 系统软件简介 07 iOS操作系统
ios·青少年编程·操作系统·系统软件
90后的晨仔13 小时前
RxSwift 框架解析
前端·ios
Humbunklung13 小时前
Rust Floem UI 框架使用简介
开发语言·ui·rust
大熊猫侯佩17 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
可爱小仙子17 小时前
ios苹果系统,js 滑动屏幕、锚定无效
前端·javascript·ios
未来猫咪花18 小时前
# Flutter状态管理对比:view_model vs Riverpod
flutter·ios·android studio
咕噜企业签名分发-淼淼21 小时前
开发源码搭建一码双端应用分发平台教程:逐步分析注意事项
android·ios
CodeCraft Studio1 天前
【案例分享】如何借助JS UI组件库DHTMLX Suite构建高效物联网IIoT平台
javascript·物联网·ui
插件开发1 天前
免费插件集-illustrator插件-Ai插件-随机填色
ui·illustrator