首页 > 解决方案 > 如何测试使用 DispatchQueue.main.async 调用的方法?

问题描述

在代码中我这样做:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    updateBadgeValuesForTabBarItems()
}

private func updateBadgeValuesForTabBarItems() {
    DispatchQueue.main.async {
        self.setBadge(value: self.viewModel.numberOfUnreadMessages, for: .threads)
        self.setBadge(value: self.viewModel.numberOfActiveTasks, for: .tasks)
        self.setBadge(value: self.viewModel.numberOfUnreadNotifications, for: .notifications)
    }
}

并在测试中:

func testViewDidAppear() {
    let view = TabBarView()
    let model = MockTabBarViewModel()
    let center = NotificationCenter()
    let controller = TabBarController(view: view, viewModel: model, notificationCenter: center)
    controller.viewDidLoad()
    XCTAssertFalse(model.numberOfActiveTasksWasCalled)
    XCTAssertFalse(model.numberOfUnreadMessagesWasCalled)
    XCTAssertFalse(model.numberOfUnreadNotificationsWasCalled)
    XCTAssertFalse(model.indexForTypeWasCalled)
    controller.viewDidAppear(false)
    XCTAssertTrue(model.numberOfActiveTasksWasCalled) //failed
    XCTAssertTrue(model.numberOfUnreadMessagesWasCalled) //failed
    XCTAssertTrue(model.numberOfUnreadNotificationsWasCalled) //failed
    XCTAssertTrue(model.indexForTypeWasCalled) //failed
}

但是我最近的四个断言都失败了。为什么?我怎样才能成功地测试它?

标签: iosswiftxctest

解决方案


我认为测试这个的最好方法是模拟DispatchQueue. 您可以创建一个协议来定义您要使用的功能:

protocol DispatchQueueType {
    func async(execute work: @escaping @convention(block) () -> Void)
}

现在扩展DispatchQueue以符合您的协议,例如:

extension DispatchQueue: DispatchQueueType {
    func async(execute work: @escaping @convention(block) () -> Void) {
        async(group: nil, qos: .unspecified, flags: [], execute: work)
    }
}

请注意,我必须从协议中省略您在代码中未使用的参数,例如groupqosflags,因为协议不允许使用默认值。这就是扩展必须显式实现协议功能的原因。

现在,在您的测试中,创建一个DispatchQueue符合该协议的模拟并同步调用闭包,例如:

final class DispatchQueueMock: DispatchQueueType {
    func async(execute work: @escaping @convention(block) () -> Void) {
        work()
    }
}

现在,您需要做的就是相应地注入队列,可能在视图控制器中init,例如:

final class ViewController: UIViewController {
    let mainDispatchQueue: DispatchQueueType

    init(mainDispatchQueue: DispatchQueueType = DispatchQueue.main) {
        self.mainDispatchQueue = mainDispatchQueue
        super.init(nibName: nil, bundle: nil)
    }

    func foo() {
        mainDispatchQueue.async {
            *perform asynchronous work*
        }
    }
}

最后,在您的测试中,您需要使用模拟调度队列创建视图控制器,例如:

func testFooSucceeds() {
    let controller = ViewController(mainDispatchQueue: DispatchQueueMock())
    controller.foo()
    *assert work was performed successfully*
}

由于您在测试中使用了模拟队列,因此代码将同步执行,您无需沮丧地等待期望。


推荐阅读