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)
}
}