I tried writing unit test for requestAuthorization by generating a mock for HKHealthStore. But I got an error. Asynchronous wait failed: Exceeded timeout of 2 seconds, with unfulfilled expectations: "Successfully tested requestAuthorization by returning true.".
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)
}
}
2
Answers
The simple solution is that you need to assign something to
stubbedRequestAuthorizationCompletionResult
before invoking thesut
‘s method.But really, what are you testing here? It looks like the only thing that this tests is whether the SUT is properly connected to the Mock, which only happens in the test, not in production code. In other words, the test only tests itself.
This is a pointless test.
Here is my recreation of the code excerpt you want to test (with some automated refactoring by AppCode):
The challenge is that almost all of this method is a closure, called asynchronously. Writing tests for this is much like microtesting network communication:
The trick to testing a closure is to capture the closure. Then your test can call the closure with different inputs. To capture this closure, we don’t want this to call the real
HKHealthStore
. Instead, we want a Test Double that replaces the method. Let’s continue your approach of using Subclass and Override. So in my test code, I write this spy. Its only job is to capture arguments. Using a tuple is a good idea — I add names to the tuple elements.To pretend that HealthKit got some kind of error, my tests define this type:
Now we can begin defining our test suite.
Note: Your code assumes that the Bool argument to the completion handler means "authorized". But the Apple documentation says otherwise. So for
authorizationCompletionArgs
tuple array, I named the first argumentrequestShown
instead ofauthorized
.The first test is whether our method calls the
HKHealthStore
method. No need for anything in the test closure. Just, "Are we calling it once? Are we passing the health categories to bothtoShare
andtoRead
?"(Since we have a single test with multiple assertions, I add a description to each assertion. That way if there is a failure, the failure message will tell me which assertion it was.)
Now we want to test the closure. To do that, we first call the method under test. The spy captures the closure. This is still the Arrange part of the test. Then comes the Act part: call the captured closure with whatever arguments we want.
First, let’s do the non-error case. It has two possibilities: success, or failure. Since this is represented by a
Bool
, let’s use two tests.(We are comparing optional
Bool
values, so we can’t useXCTAssertTrue
orXCTAssertFalse
. Instead, we can useXCTAssertEqual
comparing againsttrue
orfalse
.)These pass. It’s still important to see them fail, so I temporarily comment out the production code call to
completion(authorized, nil)
.Now we can write the last test, for the error case.
This fails, revealing a bug in your code:
Oops, the completion handler is being called twice! (There is no
return
, so it falls through.) This is why it’s important for tests not to mimic production code.So there you have it. We need 4 test cases to express all the details:
This is async code being tested synchronously by having the tests call the closure. It’s super-fast, with no need for XCTest expectations.