单元测试请求HealthKit 的授权

Unit Test requestAuthorization for HealthKit

提问人:Shak Feizi 提问时间:12/1/2022 最后编辑:Jon ReidShak Feizi 更新时间:12/15/2022 访问量:310

问:

我尝试通过为 HKHealthStore 生成模拟来为 requestAuthorization 编写单元测试。但是我遇到了一个错误。异步等待失败:超过 2 秒的超时,预期未实现:“通过返回 true 成功测试了 requestAuthorization。

func requestAuthorization(completion: @escaping(Bool?, HealthError?) -> Void) {
        self.healthStore?.requestAuthorization(toShare: self.allTypes as? Set<HKSampleType>, read: self.allTypes, completion: { authorized, error in
            if error != nil {
                print("ERROR: \(error)")
                completion(nil, .unableToAuthorizeAccess)
            }
            completion(authorized, nil)
        })
}
func testRequestAuthorization_CanReturnTrue() {
        let expectation = expectation(description: "Successfully tested requestAuthorization by returning true.")
        sut?.requestAuthorization { authorized, error in
            if error != nil {
                print(error!)
            }
            guard let authorized = authorized else { return }
            XCTAssertTrue(authorized)
            expectation.fulfill()
        }
        wait(for: [expectation], timeout: 2)
}

override func requestAuthorization(toShare typesToShare: Set<HKSampleType>?, read typesToRead: Set<HKObjectType>?, completion: @escaping (Bool, Error?) -> Void) {
        invokedRequestAuthorization = true
        invokedRequestAuthorizationCount += 1
        invokedRequestAuthorizationParameters = (typesToShare, typesToRead)
        invokedRequestAuthorizationParametersList.append((typesToShare, typesToRead))
        if let result = stubbedRequestAuthorizationCompletionResult {
            print("RESULT: \(result)")
            completion(result.0, result.1)
        }
}

iOS Swift Closures XCest 健康套件

评论

0赞 teja_D 12/1/2022
您是否尝试过将超时递增到 10 秒或更长时间?如果没有,请尝试
0赞 teja_D 12/1/2022
你能添加你的模拟代码吗?
0赞 burnsi 12/1/2022
看来失败的原因在于.请添加该方法的代码。这个问题无法以其他方式回答。sut?.requestAuthorization
0赞 Community 12/1/2022
请提供足够的代码,以便其他人可以更好地理解或重现问题。
0赞 Shak Feizi 12/2/2022
我确实将超时增加到 10 分钟以上。

答:

-1赞 Daniel T. 12/2/2022 #1

简单的解决方案是,在调用 的方法之前,您需要分配一些东西。stubbedRequestAuthorizationCompletionResultsut

但实际上,你在这里测试什么?看起来这测试的唯一内容是 SUT 是否正确连接到 Mock,这只发生在测试中,而不是在生产代码中。换句话说,测试只测试自己。

这是一个毫无意义的测试。

评论

0赞 matt 12/12/2022
对于OP来说,测试OP是否确实调用了健康存储,并且实际上以所需的方式响应了健康存储的响应,这并非毫无意义。OP 实际上可能没有这样做,但一个有用的答案是向 OP 展示如何做到这一点。标准技术是将 Apple 的 HKHealthStore 包装在一个协议中,以便您可以替换自己也符合该协议的对象。OP 需要声明为该协议获取参数。requestAuthorizationrequestAuthorizationrequestAuthorization
0赞 Daniel T. 12/12/2022
如果你将 Apple 的 HKHealthStore 包装在一个协议中,并在测试中替换你自己的对象,那么你就不是在测试它是否“实际上调用了健康商店的”。您只是在测试模拟是否正确连接到测试。IE,您正在测试测试本身,而不是生产代码。向提问者解释如何做一些没有用的事情是没有用的。requestAuthorization
0赞 Jon Reid 12/11/2022 #2

以下是我对您要测试的代码摘录的重新创建(通过 AppCode 进行一些自动重构):

import HealthKit

enum HealthError {
    case unableToAuthorizeAccess
}

class MyClass {
    var healthStore: HKHealthStore? = HKHealthStore()
    var allTypes: Set<HKObjectType> = Set()

    func requestAuthorization(completion: @escaping (Bool?, HealthError?) -> Void) {
        self.healthStore?.requestAuthorization(toShare: self.allTypes as? Set<HKSampleType>, read: self.allTypes) { authorized, error in
            if error != nil {
                print("ERROR: \(String(describing: error))")
                completion(nil, .unableToAuthorizeAccess)
            }
            completion(authorized, nil)
        }
    }
}

挑战在于,几乎所有这种方法都是一个闭包,异步调用。为此编写测试很像微测试网络通信:

  • 我们发送的请求是否正确?
  • 会发生什么不同的反应?

测试闭包的诀窍是捕获闭包。然后,您的测试可以使用不同的输入调用闭包。为了捕获此闭包,我们不希望将其称为真正的 .相反,我们需要一个替换该方法的 Test Double。让我们继续使用 Subclass 和 Override 的方法。所以在我的测试代码中,我写了这个间谍。它唯一的工作就是捕捉参数。使用元组是个好主意——我为元组元素添加名称。HKHealthStore

class HKHealthStoreSpy: HKHealthStore {
    var requestAuthorizationArgs: [(typesToShare: Set<HKSampleType>?, typesToRead: Set<HKObjectType>?, completion: (Bool, Error?) -> Void)] = []

    override func requestAuthorization(toShare typesToShare: Set<HKSampleType>?, read typesToRead: Set<HKObjectType>?, completion: @escaping (Bool, Error?) -> Void) {
        requestAuthorizationArgs.append((typesToShare, typesToRead, completion))
    }
}

为了假装 HealthKit 出现了某种错误,我的测试定义了这种类型:

enum SomeError: Error {
    case problem
}

现在我们可以开始定义我们的测试套件了。

final class MyClassTests: XCTestCase {
    private var healthStoreSpy: HKHealthStoreSpy!
    private var sut: MyClass!
    private var authorizationCompletionArgs: [(requestShown: Bool?, error: HealthError?)] = []

    override func setUpWithError() throws {
        try super.setUpWithError()
        healthStoreSpy = HKHealthStoreSpy()
        sut = MyClass()
        sut.healthStore = healthStoreSpy
    }
    
    override func tearDownWithError() throws {
        authorizationCompletionArgs = []
        healthStoreSpy = nil
        sut = nil
        try super.tearDownWithError()
    }

注意:您的代码假定完成处理程序的 Bool 参数表示“已授权”。但苹果文档却不这么认为。因此,对于元组数组,我命名了第一个参数而不是 。authorizationCompletionArgsrequestShownauthorized

第一个测试是 我们的方法是否调用了该方法。测试闭合中不需要任何东西。只是,“我们叫一次吗?我们是否将健康类别传递给两者?HKHealthStoretoSharetoRead

    func test_requestAuthorization_requestsToShareAndReadAllTypes() throws {
        let healthCategories = Set([HKObjectType.workoutType()])
        sut.allTypes = healthCategories
        
        sut.requestAuthorization { _, _ in }
        
        XCTAssertEqual(healthStoreSpy.requestAuthorizationArgs.count, 1, "count")
        XCTAssertEqual(healthStoreSpy.requestAuthorizationArgs.first?.typesToRead, healthCategories, "typesToRead")
        XCTAssertEqual(healthStoreSpy.requestAuthorizationArgs.first?.typesToShare, healthCategories, "typesToShare")
    }

(由于我们有一个包含多个断言的单个测试,因此我为每个断言添加一个描述。这样,如果出现故障,失败消息将告诉我这是哪个断言。

现在我们要测试闭包。为此,我们首先调用被测方法。间谍捕捉到了闭合。这仍然是测试的安排部分。然后是 Act 部分:用我们想要的任何参数调用捕获的闭包。

首先,让我们来做非错误情况。它有两种可能性:成功或失败。由于这是由 a 表示的,因此我们使用两个测试。Bool

    func test_requestAuthorization_successfullyShowedRequest() throws {
        sut.requestAuthorization { [self] authorization, error in
            authorizationCompletionArgs.append((authorization, error))
        }

        healthStoreSpy.requestAuthorizationArgs.first?.completion(true, nil)

        XCTAssertEqual(authorizationCompletionArgs.count, 1, "count")
        XCTAssertEqual(authorizationCompletionArgs.first?.requestShown, true, "requestShown")
        XCTAssertNil(authorizationCompletionArgs.first?.error, "error")
    }

    func test_requestAuthorization_requestNotShownButMissingError() throws {
        sut.requestAuthorization { [self] authorization, error in
            authorizationCompletionArgs.append((authorization, error))
        }

        healthStoreSpy.requestAuthorizationArgs.first?.completion(false, nil)

        XCTAssertEqual(authorizationCompletionArgs.count, 1, "count")
        XCTAssertEqual(authorizationCompletionArgs.first?.requestShown, false, "requestShown")
        XCTAssertNil(authorizationCompletionArgs.first?.error, "error")
    }

(我们正在比较可选的 Bool,因此我们不能使用 or 。相反,我们可以使用比较 或 。XCTAssertTrueXCTAssertFalseXCTAssertEqualtruefalse

这些都过去了。看到它们失败仍然很重要,所以我暂时注释掉了对 的生产代码调用。completion(authorized, nil)

现在我们可以为错误情况编写最后一个测试。

    func test_requestAuthorization_failedToShowRequest() throws {
        sut.requestAuthorization { [self] authorization, error in
            authorizationCompletionArgs.append((authorization, error))
        }

        healthStoreSpy.requestAuthorizationArgs.first?.completion(false, SomeError.problem)

        XCTAssertEqual(authorizationCompletionArgs.count, 1, "count")
        XCTAssertNil(authorizationCompletionArgs.first?.requestShown, "requestShown")
        XCTAssertEqual(authorizationCompletionArgs.first?.error, .unableToAuthorizeAccess, "error")
    }

此操作失败,并显示代码中的一个错误:

XCTAssertEqual 失败:(“2”) 不等于 (“1”) - 计数

哎呀,完成处理程序被调用了两次!(没有,所以它失败了。这就是为什么测试不要模仿生产代码很重要的原因。return

所以你有它。我们需要 4 个测试用例来表达所有细节:

  1. 它是否使用我们想要的参数请求 HealthKit 授权?
  2. 如果请求已成功显示给用户,会发生什么情况?
  3. 如果未显示请求,但没有错误,会发生什么情况?从 Apple 的文档来看,尚不清楚这是否会发生——这是一个奇怪的 API。
  4. 如果我们遇到错误会怎样?

这是通过让测试调用闭包来同步测试的异步代码。它速度超快,不需要 XCTest 的期望。