簡體   English   中英

如何測試使用 DispatchQueue.main.async 調用的方法?

[英]How to test method that is called with 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
}

但是我最近的四個斷言都失敗了。 為什么? 我怎樣才能成功測試它?

我認為測試這個的最好方法是模擬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*
}

由於您在測試中使用了模擬隊列,因此代碼將同步執行,您無需沮喪地等待期望。

您不需要在主隊列上調用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)
}

但這不是一個好習慣。

這是一個關於如何實現它的概念的小證明:

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: (() -> ())?
}

在這種情況下,您可以通過檢查當前線程是否為主線程並同步執行代碼來輕松實現這一點。

例如在演示者中,我以這種方式更新視圖:

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

要測試異步代碼,您應該修改updateBadgeValuesForTabBarItems函數並使用完成閉包直接從測試中調用它:

func updateBadgeValuesForTabBarItems(completion: (() -> Void)? = nil) {
    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)
        completion?()
    }
}

現在您可以像以前一樣在常規代碼中調用此函數,例如: updateBadgeValuesForTabBarItems() 但是對於測試,您可以添加完成閉包並使用XCTestExpectation等待:

func testBadge() {
    
    ...

    let expectation = expectation(description: "Badge") 

    updateBadgeValuesForTabBarItems {
        XCTAssertTrue(model.numberOfActiveTasksWasCalled)
        XCTAssertTrue(model.numberOfUnreadMessagesWasCalled)
        XCTAssertTrue(model.numberOfUnreadNotificationsWasCalled)
        XCTAssertTrue(model.indexForTypeWasCalled)
        
        expectation.fulfill()
    }

    wait(for: [expectation], timeout: 1)
}

你應該

  1. 注入的依賴(DispatchQueue)到您的視圖控制器,這樣就可以在測試中改變
  2. 使用協議反轉依賴,以更好地符合 SOLID 原則(接口隔離和依賴反轉)
  3. 在您的測試中模擬DispatchQueue,以便您可以控制您的場景

讓我們應用這三個項目:

為了反轉依賴關系,我們需要一個抽象類型,即在 Swift 中,一個協議。 然后我們擴展 DispatchQueue 以符合該協議

protocol Dispatching {
    func async(execute workItem: DispatchWorkItem)
}

extension DispatchQueue: Dispatching {}

接下來,我們需要將依賴項注入到我們的視圖控制器中。 這意味着,將任何正在調度的東西傳遞給我們的視圖控制器

final class MyViewController {
    // MARK: - Dependencies
    
    private let dispatchQueue: Dispatching // Declading that our class needs a dispatch queue

    // MARK: - Initialization

    init(dispatchQueue: Dispatching = DispatchQueue.main) { // Injecting the dependencies via constructor
        self.dispatchQueue = dispatchQueue
        super.init(nibName: nil, bundle: nil) // We must call super 
    }

    @available(*, unavailable)
    init(coder aCoder: NSCoder?) {
        fatalError("We should only use our other init!")
    }

    // MARK: - View lifecycle

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

    // MARK: - Private methods
    private func updateBadgeValuesForTabBarItems() {
        dispatchQueue.async { // Using our dependency instead of DispatchQueue directly
            self.setBadge(value: self.viewModel.numberOfUnreadMessages, for: .threads)
            self.setBadge(value: self.viewModel.numberOfActiveTasks, for: .tasks)
            self.setBadge(value: self.viewModel.numberOfUnreadNotifications, for: .notifications)
        }
    }
}

最后,我們需要為我們的測試創建一個模擬。 在這種情況下,通過遵循測試 doubles ,我們應該創建一個 Fake,即一個 DispatchQueue 模擬,它實際上在生產中不起作用,但在我們的測試中起作用

final class DispatchFake: Dispatching {
    func async(execute workItem: DispatchWorkItem) {
        workItem.perform()
    }
}

當我們在測試時,我們需要做的就是創建我們的被測系統(在這種情況下是控制器),傳遞一個假的調度實例

我在我的測試中使用了DispatchQueue.main.asyncAfter()和期望,否則它在文本可以設置在DispatchQueue.main.async {}之前失敗

測試方法:

func setNumpadTexts(_ numpad: NumericalKeyboardVC) {
   numpad.setTexts(belowNumberLabelText: Currency.symbol, enterKeyText: NSLocalizedString("Add", comment:""))
}    

func setTexts(belowNumberLabelText: String? = "", enterKeyText: String) {
   DispatchQueue.main.async {
       self.belowNumberDisplayLbl.text = belowNumberLabelText
       self.enterBtn.setTitle(enterKeyText, for: .normal)
   }
}

測試:

func testSetNumpadTexts() {
    sut.setNumpadTexts(numpad)
    
    let expectation = expectation(description: "TextMatching")
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: {
        //then
        XCTAssertEqual(self.numpad.enterBtn.title(for: .normal), NSLocalizedString("Add", comment:""))
        XCTAssertEqual(self.numpad.belowNumberDisplayLbl.text, Currency.symbol)
        
        expectation.fulfill()
    })
    wait(for: [expectation], timeout: 2.0)
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM