I want the test below to pass. In my real code, AsyncClass
is dispatching work to multiple queue's using the DispatchGroup
class. In Swift we work with @escaping
completion handlers for async work. I am looking for a way to make the calling thread wait for the dispatchGroup
to finish working. The closure could go away that way.
So long story short: I have a calling thread (main) that is calling a function that is dispatching work to multiple queue's. I want the calling thread to be blocked while that work is going on and be unblocked when the work is done. This results in that the @escaping completionHandler can go away and I can call the function normally without a closure (and when I go to the next line after the method call, the work is completely done ofcourse)
I use this code only for tests, I know I should never block the main thread in production.
Usecase: I have tests which call an expensive method. That method is doing work like ‘AsyncClass’ in this example. Inside AsyncClass I am dispatching work to some threads to speed things up. Now while testing, I can create expectations all the time and call this method with a closure, but that’s to verbose for me. I want to turn an async method to a sync method, for the ease of use.
import XCTest
class DispatchGroupTestTests: XCTestCase {
func testIets() {
let clazz = AsyncClass()
var isCalled = false
clazz.doSomething {
isCalled = true
}
XCTAssert(isCalled)
}
}
class AsyncClass {
func doSomething(completionHandler: @escaping () -> ()) {
let dispatchGroup = DispatchGroup()
for _ in 0...5 {
dispatchGroup.enter()
DispatchQueue.global().async {
let _ = (0...10000).map { $0 * 1000 }
dispatchGroup.leave()
}
}
dispatchGroup.wait() // Doesn't work
dispatchGroup.notify(queue: .main) {
completionHandler()
}
}
}
I managed to do it spinlock-way (just sleep the thread in a while loop):
import XCTest
class DispatchGroupTestTests: XCTestCase {
func testIets() {
let clazz = AsyncClass()
var isCalled = false
clazz.doSomething {
isCalled = true
}
XCTAssert(isCalled)
}
}
class AsyncClass {
func doSomething(completionHandler: () -> ()) {
var isDone = false
let dispatchGroup = DispatchGroup()
for _ in 0...5 {
dispatchGroup.enter()
DispatchQueue.global().async {
let _ = (0...1000000).map { $0 * 10000 }
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
isDone = true
}
while !isDone {
print("sleepy")
Thread.sleep(forTimeInterval: 0.1)
}
completionHandler()
}
}
Now, the closure isn't needed anymore, and the expectations can be removed (although I don't have them in the provided examples, I can omit them in my 'real' testing code):
import XCTest
class DispatchGroupTestTests: XCTestCase {
var called = false
func testIets() {
let clazz = AsyncClass()
clazz.doSomething(called: &called)
XCTAssert(called)
}
}
class AsyncClass {
func doSomething(called: inout Bool) {
var isDone = false
let dispatchGroup = DispatchGroup()
for _ in 0...5 {
dispatchGroup.enter()
DispatchQueue.global().async {
let _ = (0...1000000).map { $0 * 10000 }
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
isDone = true
}
while !isDone {
print("sleepy")
Thread.sleep(forTimeInterval: 0.1)
}
called = true
}
}
So I did a performance test with the measure block and my expectations (not XCTestCase expectations) become fulfilled: it is quicker to dispatch work to other threads and block the calling thread in a spinlock, comparing to dispatch all the work to the calling thread (taking into account we don't want escaping blocks and we want everything sync, just for easy calling functions inside test methods). I literally just filled in random computations and this was the result:
import XCTest
let work: [() -> ()] = Array.init(repeating: { let _ = (0...1000000).map { $0 * 213123 / 12323 }}, count: 10)
class DispatchGroupTestTests: XCTestCase {
func testSync() {
measure {
for workToDo in work {
workToDo()
}
}
}
func testIets() {
let clazz = AsyncClass()
measure {
clazz.doSomething()
}
}
}
class AsyncClass {
func doSomething() {
var isDone = false
let dispatchGroup = DispatchGroup()
for workToDo in work {
dispatchGroup.enter()
DispatchQueue.global().async {
workToDo()
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
isDone = true
}
while !isDone {
Thread.sleep(forTimeInterval: 0.1)
}
}
}