2

试图模拟 CKDatabase 但它没有可访问的初始化程序。有没有另一种方法来创建一个假的 CKDatabase 对象?

此代码返回“错误:无法覆盖已标记为不可用的'init'”

class MockDatabase : CKDatabase
{
    override func saveRecord(record: CKRecord!, completionHandler: ((CKRecord!, NSError!) -> Void)!) { }

    override func deleteRecordWithID(recordID: CKRecordID!, completionHandler: ((CKRecordID!, NSError!) -> Void)!) { }

    override func fetchRecordWithID(recordID: CKRecordID!, completionHandler: ((CKRecord!, NSError!) -> Void)!) { }

    override func deleteSubscriptionWithID(subscriptionID: String!, completionHandler: ((String!, NSError!) -> Void)!) { }

    override func saveSubscription(subscription: CKSubscription!, completionHandler: ((CKSubscription!, NSError!) -> Void)!) {}
}

云套件框架似乎是单元测试的完美候选者,因为有很多可能的错误可以从服务器返回,并确保您的应用程序可以一致地处理它们,每次迭代都会很棒。有没有人找到解决这个问题的方法?

4

3 回答 3

1

由于 CKDatabase init 不可用,您将永远无法创建它的实例。因此,创建 MockDatabase 毫无意义,因为您将永远无法创建它的实例。进行某种模拟的唯一方法是围绕 CKDatabase 功能创建自己的 DAO 类包装器,并且只使用该 DAO 调用 CKDatabase 函数。然后你可以模拟那个 DAO。您甚至可以为该 DAO 添加额外的功能,但是您将无法对其进行测试。

于 2014-08-26T06:18:52.537 回答
0

所有 CloudKit 类都是@objc(它们都派生自 NSObject。)这意味着它们使用动态调度。在 Objective-C 中,您可以通过实现一个新的 Obj-C 类 MockCKDatabase 来模拟 CKDatabase,该类是 NSObject 的子类并实现 CKDatabase 的所有公共方法(或者至少是您使用的所有方法。)然后您实际上只需实例化一个并将指针从MockCKDatabase*CKDatabase*.

我从来没有在 Swift 中做过这种事情,但我想如果你只是做了同样的步骤,它应该可以工作。

于 2020-09-03T23:36:54.457 回答
-1

我知道这是一篇旧文章,但 CloudKit 的测试仍然是一个长期存在的问题。我创建了一个模拟框架来测试 CloudKit 的一些主要功能(仅针对 iOS 15.0 及更高版本构建和测试 - 以利用迁移到更清洁的 CKDatabaseOperation 操作)。代码(使用 DocC 完整记录)和测试在这里:https ://github.com/ccavnor/MockCloudKitTesting

以下是如何使用它的示例:

将框架添加到您的项目

下载框架并单击 Xcode 项目的根文件夹以打开项目编辑器。单击您的项目目标并转到“常规”选项卡的“框架、库和嵌入式内容”部分。在此处导入 MockCloudKitFramework。这与 CloudKit 框架应该已经导入到您的项目中的位置相同。

导入框架

在您已导入 CloudKit 的模块中,添加以下导入语句:

import MockCloudKitFramework

添加类型别名

将以下类型别名块添加到模块中。确保将“#if DEBUG”语句保留在那里。它确保类型别名仅在测试和调试运行期间处于活动状态。MockCloudKitFramework 本身被包裹在其中,因此在正常运行时不会错误地访问模拟对象。这些类型别名仅允许您通过它们模拟的 CloudKit 对象的名称来调用 MockCloudKitFramework 模拟对象。

#if DEBUG
    typealias CKContainer = MockCKContainer
    typealias CKDatabase = MockCKDatabase
    typealias CKModifyRecordsOperation = MockCKModifyRecordsOperation
    typealias CKFetchRecordsOperation = MockCKFetchRecordsOperation
    typealias CKQueryOperation = MockCKQueryOperation
#endif

像使用 CloudKit 一样使用它

func test_CKModifyRecordsOperation_modifyRecordsResultBlock_success() {
    // really a MockCKContainer
    let cloudContainer: CKContainer = CKContainer.default()
    // really a MockCKDatabase
    let database: CKDatabase = cloudContainer.publicCloudDatabase
    let expect = expectation(description: "CKModifyRecordsOperation")
    var records: [CKRecord] = [CKRecord]()
    records.append(makeCKRecord())
    records.append(makeCKRecord())
    // really a MockCKModifyRecordsOperation
    let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
    operation.modifyRecordsResultBlock = { result in
        switch result {
        case .success:
            expect.fulfill()
        case .failure(let error):
            XCTAssertNil(error) // shouldn't be reached
        }
    }
    database.add(operation)
}

请注意,modifyRecordsResultBlock 被注册为回调,正如您所期望的那样。但是您在 recordsToSave 属性上设置的记录也会添加到 MockCKDatabase - 并且仅添加到您请求的范围(在本例中为 .public):

   // really a MockCKFetchRecordsOperation
   let operation = CKFetchRecordsOperation(recordIDs: recordIds)
   operation.perRecordResultBlock = { recordId, _ in
       print("got record with id ---> \(recordId)")
   }
   database.add(operation)

随心所欲地使用它,你可以使用 CloudKit

这一切都很好,但是模拟测试的真正目的是测试成功(上图)和失败案例。MockCloudKitFramework 允许您在 CKContainer(请参阅文档)或 CKDatabaseOperation 子类上设置我们要测试的错误。假设我们想测试我们的代码如果 CloudKit 没有遇到内部错误会发生什么。我们可以在 CKDatabaseOperation 实例上设置错误

let error = NSError(domain: "CKError", code: CKError.Code.internalError, userInfo: nil)
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
operation.setError = error

或者在类型上设置

CKModifyRecordsOperation.setError = error

但是现在错误将作为失败条件出现在我们的完成块中:

// really a MockCKModifyRecordsOperation
operation.modifyRecordsResultBlock = { result in
   switch result {
   case .success:
       // won't get here
   case .failure(let error):
       // error contains CKError.internalError code
   }
}
// Add the operation to the database to run it
database.add(operation)
于 2022-02-15T19:24:35.487 回答