如何测试使用DispatchQueue.main.async调用的方法?

时间:2018-10-26 05:42:52

标签: ios swift xctest

在代码中,我这样做是这样的:

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
}

但是我最近的四个断言都失败了。为什么?如何成功测试呢?

4 个答案:

答案 0 :(得分:4)

我认为最好的测试方法是模拟class YourAdmin(admin.ModelAdmin) def get_changeform_initial_data(self, request): return {'owner': request.user} 。您可以创建定义要使用的功能的协议:

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) } } groupqos,因为协议不允许使用默认值。这就是扩展必须显式实现协议功能的原因。

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

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*
        }
    }
}

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

答案 1 :(得分:1)

您不需要在主队列的updateBadgeValuesForTabBarItems方法中调用代码。

但是,如果您确实需要它,可以执行以下操作:

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)
    let expectation = self.expectation(description: "Test")
    DispatchQueue.main.async {
        expectation.fullfill()
    }
    self.waitForExpectations(timeout: 1, handler: nil)
    XCTAssertTrue(model.numberOfActiveTasksWasCalled)
    XCTAssertTrue(model.numberOfUnreadMessagesWasCalled)
    XCTAssertTrue(model.numberOfUnreadNotificationsWasCalled)
    XCTAssertTrue(model.indexForTypeWasCalled)
}

但这不是一个好习惯。

答案 2 :(得分:0)

以下是有关如何实现的一个小概念证明:

func testExample() {
        let expectation = self.expectation(description: "numberOfActiveTasks")
        var mockModel = MockModel()
        mockModel.numberOfActiveTasksClosure = {() in
            expectation.fulfill()
        }

        DispatchQueue.main.async {
            _ = mockModel.numberOfActiveTasks
        }


        self.waitForExpectations(timeout: 2, handler: nil)
    }

这是MockModel

struct MockModel : Model {
    var numberOfActiveTasks: Int {
        get {
            if let cl = numberOfActiveTasksClosure {
                cl()
            }
            //we dont care about the actual value for this test
            return 0
        }
    }
    var numberOfActiveTasksClosure: (() -> ())?
}

答案 3 :(得分:0)

在这种情况下,您可以通过检查当前线程是否为主线程并同步执行代码来轻松实现这一点。

例如在演示者中,我以这种方式更新视图:

  private func updateView(with viewModel: MyViewModel) {
    if Thread.isMainThread {
      view?.update(with: viewModel)
    } else {
      DispatchQueue.main.async {
        self.view?.update(with: viewModel)
      }
    }
  }

然后我可以为我的演示者编写同步单元测试:

  func testOnViewDidLoadFetchFailed() throws {
    presenter.onViewDidLoad() 
    // presenter is calling interactor.fetchData when onViewDidLoad is called

    XCTAssertEqual(interactor.fetchDataCallsCount, 1)
    
    // test execute fetchData completion closure manually in the main thread
    interactor.fetchDataCalls[0].completion(.failure(TestError())) 

    // presenter will call updateView(viewModel:) internally in synchronous way
    // because we have check if Thread.isMainThread in updateView(viewModel:)
    XCTAssertEqual(view.updateCallsCount, 1)
    guard case .error = view.updateCalls[0] else {
      XCTFail("error expected, got \(view.updateCalls[0])")
      return
    }
  }