Reputation: 3578
supposed I have these lines of code:
func reset() {
initializeAnythingElse() {
// AnythingElse
}
initializeHomeData() {
// HomeData
}
}
func initializeHomeData(callback: @escaping (()-> Void)) {
getHomeConfig() {
callback()
}
}
func initializeAnythingElse(callback: @escaping (()-> Void)) {
getAnythingElse() {
callback()
}
}
and I would like to write a unit test for that code. For initializeHomeData
and initializeAnythingElse
, I can write the unit test like :
func testInitializeHomeData() {
let successExpectation = expectation(description: "")
sut.initializeHomeData {
successExpectation.fulfill()
}
waitForExpectations(timeout: 1.0, handler: nil)
// Validation goes here
}
func testInitializeAnythingElse() {
let successExpectation = expectation(description: "")
sut.initializeAnythingElse {
successExpectation.fulfill()
}
waitForExpectations(timeout: 1.0, handler: nil)
// Validation goes here
}
My question is, how to test reset()
? Should I just call them inside testReset()
like :
func testReset() {
testInitializeHomeData()
testInitializeAnythingElse()
}
but I think this is not the proper implementation for that.
Upvotes: 8
Views: 3394
Reputation: 10112
Go ahead and check XCTestExpectation
out.
This is the recommended api for async testing. Steps -
Create an instance of XCTestExpectation
Call your asynchronous method
Call waitForExpectations(timeout: timeout, handler: handler)
Step 3 provides a waiting time for async call to complete. Whenever your async method completion gets called, call .fulfill()
on instance of XCTestExpectation
For your reference -
XCTest and asynchronous testing in Xcode 6
Pasting the code from above answer here in case the link stops working later -
func testFetchNews() {
let expectation = self.expectationWithDescription("fetch posts")
Post.fetch(.Top, completion: {(posts: [Post]!, error: Fetcher.ResponseError!) in
XCTAssert(true, "Pass")
expectation.fulfill()
})
self.waitForExpectationsWithTimeout(5.0, handler: nil)
}
Hop this helps.
Upvotes: 0
Reputation: 4259
Add callback to reset
method:
func reset(callback: @escaping (()-> Void)) {
var callbacksCount = 0
func tryCallback() {
if callbacksCount == 2 {
callback()
}
}
initializeAnythingElse() {
callbacksCount += 1
}
initializeHomeData() {
callbacksCount += 1
}
}
and test it the same way as you did for other methods.
You also can make calls sequential, parallel execution not seems like an optimization worth more complex code:
func reset(callback: @escaping (()-> Void)) {
initializeAnythingElse() {
initializeHomeData() {
callback()
}
}
}
Upvotes: 0
Reputation: 2459
You are right. To test reset
you need to call reset
, and not it's internal methods.
That being said, reset
is currently written in a way that makes it untestable. The reason you are able to test the other standalone methods so easily is because of the callback
argument both accepts.
I would recommend you rewrite reset to allow two optional callbacks as follows:
typealias Callback = () -> ()
func reset(
homeDataCallback: @escaping Callback? = nil,
anythingElseCallback: @escaping Callback? = nil) {
initializeAnythingElse() {
anythingElseCallback?()
}
initializeHomeData() {
homeDataCallback?()
}
}
Note that this change allows you get notified, in async, when those two internal calls complete.
Now, your test method needs to be written with some sort of synchronization primitive in mind, since logically, reset is only complete when both home data and anything else is done and their callbacks invoked.
There are many ways to achieve this, but I will show you an approach with semaphores:
func testReset() {
let expectation = expectation(description: "reset() completes within some duration")
// some mechanism to synchronize concurrent tasks
// I am using a semaphore
let s = DispatchSemaphore(value: 0)
let homeCallback: Callback = {
s.signal() // signals the completion of home data init
}
let anythingElseCallback: Callback = {
s.signal() // signals the completions of anything else setup
}
// call your reset method as part of the test
reset(homeDataCallback: homeCallback, anythingElseCallback: anythingElseCallback)
// we know we need to wait for two things to complete
// init home data and anything else, so do that
s.wait()
s.wait()
// at this step, reset's internal async methods
// have completed so we can now
// fulfill the expectation
expectation.fulfill()
}
Note that all this change is required to purely allow you to test the reset
call. Your function signature allows you to write reset()
as current in your existing code since it has optional arguments that are both set to nil for default values.
Upvotes: 10