1

Caveat - I read the few questions about testing threads but may have missed the answer so if the answer is there and I missed it, please point me in the right direction.

I want to test that a tableView call to reloadData is executed on the main queue.

This should code should result in a passing test:

var cats = [Cat]() { didSet { DispatchQueue.main.async { [weak self] in tableView.reloadData() } } } 

This code should result in a failing test:

var cats = [Cat]() { didSet { tableView.reloadData() } } 

What should the test look like?

Note to the testing haters: I know this is an easy thing to catch when you run the app but it's also an easy thing to miss when you're refactoring and adding layers of abstraction and multiple network calls and want to update the UI with some data but not other data etc etc... so please don't just answer with "Updates to UI go on the main thread" I know that already. Thanks!

3 Answers 3

1

Use dispatch_queue_set_specific function in order to associate a key-value pair with the main queue

Then use dispatch_queue_get_specific to check for the presence of key & value:

fileprivate let mainQueueKey = UnsafeMutablePointer<Void>.alloc(1) fileprivate let mainQueueValue = UnsafeMutablePointer<Void>.alloc(1) /* Associate a key-value pair with the Main Queue */ dispatch_queue_set_specific( dispatch_get_main_queue(), mainQueueKey, mainQueueValue, nil ) func isMainQueue() -> Bool { /* Checking for presence of key-value on current queue */ return (dispatch_get_specific(mainQueueKey) == mainQueueValue) } 
Sign up to request clarification or add additional context in comments.

Comments

0

I wound up taking the more convoluted approach of adding an associated Bool value to UITableView, then swizzling UITableView to redirect reloadData()

fileprivate let reloadDataCalledOnMainThreadString = NSUUID().uuidString.cString(using: .utf8)! fileprivate let reloadDataCalledOnMainThreadKey = UnsafeRawPointer(reloadDataCalledOnMainThreadString) extension UITableView { var reloadDataCalledOnMainThread: Bool? { get { let storedValue = objc_getAssociatedObject(self, reloadDataCalledOnMainThreadKey) return storedValue as? Bool } set { objc_setAssociatedObject(self, reloadDataCalledOnMainThreadKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } dynamic func _spyReloadData() { reloadDataCalledOnMainThread = Thread.isMainThread _spyReloadData() } //Then swizzle that with reloadData() } 

Then in the test I updated the cats on the background thread so I could check if they were reloaded on the main thread.

func testReloadDataIsCalledWhenCatsAreUpdated() { // Checks for presence of another associated property that's set in the swizzled reloadData method let reloadedPredicate = NSPredicate { [controller] _,_ in controller.tableView.reloadDataWasCalled } expectation(for: reloadedPredicate, evaluatedWith: [:], handler: nil) // Appends on the background queue to simulate an asynchronous call DispatchQueue.global(qos: .background).async { [weak controller] in let cat = Cat(name: "Test", identifier: 1) controller?.cats.append(cat) } // 2 seconds seems excessive but NSPredicates only evaluate once per second waitForExpectations(timeout: 2, handler: nil) XCTAssert(controller.tableView.reloadDataCalledOnMainThread!, "Reload data should be called on the main thread when cats are updated on a background thread") } 

2 Comments

The Swift 3 way to ask whether a thread is a given thread is the dispatchPrecondition(condition:) global function.
@matt I can see how you would use that in production code to ensure something is on the main thread but how would you use that in a test? I figure you'd assert that the condition is met but what would the condition be?
0

Here is an updated version of the answer provided by Oleh Zayats that I am using in some tests of Combine publishers.

extension DispatchQueue { func setAsExpectedQueue(isExpected: Bool = true) { guard isExpected else { setSpecific(key: .isExpectedQueueKey, value: nil) return } setSpecific(key: .isExpectedQueueKey, value: true) } static func isExpectedQueue() -> Bool { guard let isExpectedQueue = DispatchQueue.getSpecific(key: .isExpectedQueueKey) else { return false } return isExpectedQueue } } extension DispatchSpecificKey where T == Bool { static let isExpectedQueueKey = DispatchSpecificKey<Bool>() } 

This is an example test using Dispatch and Combine to verify it is working as expected (you can see it fail if you remove the receive(on:) operator).:

final class IsExpectedQueueTests: XCTestCase { func testIsExpectedQueue() { DispatchQueue.main.setAsExpectedQueue() let valueExpectation = expectation(description: "The value was received on the expected queue") let completionExpectation = expectation(description: "The publisher completed on the expected queue") defer { waitForExpectations(timeout: 1) DispatchQueue.main.setAsExpectedQueue(isExpected: false) } DispatchQueue.global().sync { Just(()) .receive(on: DispatchQueue.main) .sink { _ in guard DispatchQueue.isExpectedQueue() else { return } completionExpectation.fulfill() } receiveValue: { _ in guard DispatchQueue.isExpectedQueue() else { return } valueExpectation.fulfill() }.store(in: &cancellables) } } override func tearDown() { cancellables.removeAll() super.tearDown() } var cancellables = Set<AnyCancellable>() } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.