赞
踩
- test the business logic in your app : 测试应用中的业务逻辑
- test the UI of your app : 测试应用中的界面
《Testing Swift》https://www.hackingwithswift.com/store/testing-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)
- }
- }
当创建项目时,没有选择 Include Tests/包含测试 选项时,需要添加文件去对应项目,不然测试文件会报 No such module 'XCTest' 编译错误
添加单元测试文件:
方法一 : 选择项目 -> 菜单栏 Editor -> Add Target... -> 弹出对话框,选择 Test 栏下 -> Unit Testing Bundle -> 填写信息/可默认 -> Finish,完成创建单元测试文件。
方法二 : 选择项目,点击 PROJECT 列,最下的 + 按钮,弹出对话框,选择 Test 栏下 ,后面步骤与上一致
创建单元测试文件 UnitTestingBootcampViewModel_Tests.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)
- }
- }
- 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()
- }
- }
- 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)
- }
- }
- 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)
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。