提问人:Shak Feizi 提问时间:12/1/2022 最后编辑:Jon ReidShak Feizi 更新时间:12/15/2022 访问量:310
单元测试请求HealthKit 的授权
Unit Test requestAuthorization for HealthKit
问:
我尝试通过为 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)
}
}
答:
简单的解决方案是,在调用 的方法之前,您需要分配一些东西。stubbedRequestAuthorizationCompletionResult
sut
但实际上,你在这里测试什么?看起来这测试的唯一内容是 SUT 是否正确连接到 Mock,这只发生在测试中,而不是在生产代码中。换句话说,测试只测试自己。
这是一个毫无意义的测试。
评论
requestAuthorization
requestAuthorization
requestAuthorization
requestAuthorization
以下是我对您要测试的代码摘录的重新创建(通过 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 参数表示“已授权”。但苹果文档却不这么认为。因此,对于元组数组,我命名了第一个参数而不是 。authorizationCompletionArgs
requestShown
authorized
第一个测试是 我们的方法是否调用了该方法。测试闭合中不需要任何东西。只是,“我们叫一次吗?我们是否将健康类别传递给两者?HKHealthStore
toShare
toRead
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 。相反,我们可以使用比较 或 。XCTAssertTrue
XCTAssertFalse
XCTAssertEqual
true
false
这些都过去了。看到它们失败仍然很重要,所以我暂时注释掉了对 的生产代码调用。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 个测试用例来表达所有细节:
- 它是否使用我们想要的参数请求 HealthKit 授权?
- 如果请求已成功显示给用户,会发生什么情况?
- 如果未显示请求,但没有错误,会发生什么情况?从 Apple 的文档来看,尚不清楚这是否会发生——这是一个奇怪的 API。
- 如果我们遇到错误会怎样?
这是通过让测试调用闭包来同步测试的异步代码。它速度超快,不需要 XCTest 的期望。
评论
sut?.requestAuthorization