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

1mrurvl1  于 2023-05-23  发布在  iOS
关注(0)|答案(8)|浏览(152)

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

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
}

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

mctunoxg

mctunoxg1#

我认为测试这一点的最好方法是模拟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*
}

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

tmb3ates

tmb3ates2#

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

但这不是一个好的做法。

8fq7wneg

8fq7wneg3#

我在测试中使用了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)
}
kxe2p93d

kxe2p93d4#

你应该
1.注入依赖项(DispatchQueue)到视图控制器中,以便在测试中更改
1.使用协议反转依赖,更符合SOLID原则(接口分段和依赖反转)
1.在测试中模拟DispatchQueue,以便您控制场景
让我们应用这三个项目:
为了反转依赖,我们需要一个 abstract 类型,也就是说,在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)
        }
    }
}

最后,我们需要为我们的测试创建一个mock。在本例中,通过遵循testing doubles,我们应该创建一个Fake,也就是说,一个实际上不能在生产环境中工作但可以在测试中工作的DispatchQueue mock

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

当我们进行测试时,我们需要做的就是创建我们的SystemUnderTest(在本例中是控制器),传递一个假的调度示例

1hdlvixo

1hdlvixo5#

你可以通过检查当前线程是否是主线程,并在这种情况下同步执行代码来轻松实现这一点。
例如,在presenter中,我以这种方式更新视图:

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

o4hqfura6#

这里是一个小的概念证明,你可以如何实现它:

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

xnifntxz7#

要测试异步代码,你应该修改你的updateBadgeValuesForTabBarItems函数,并使用completion闭包直接从测试中调用它:

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()。但是对于测试,你可以添加一个completion闭包并使用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)
}
wwodge7n

wwodge7n8#

几年后,但也许有用。
在我的特定示例中,我使用DispatchQueue上的扩展来检测我们是否正在运行单元测试。因此,如果我们在XCTest环境中,该方法将自动执行而不进行分派。

public extension DispatchQueue {

    private static let isRunningUnitTests: Bool = {
        Thread.current.threadDictionary.allKeys.contains {
            ($0 as? String)?.range(of: "XCTest", options: .caseInsensitive) != nil
        }
    }()

    func asyncTestable(
        closure: @escaping @convention(block) () -> Void
    ) {
        let workItem = DispatchWorkItem(block: closure)
        asyncTestable(execute: workItem)
    }

    func asyncTestable(
        execute workItem: DispatchWorkItem
    ) {
        if !Self.isRunningUnitTests {
            async(execute: workItem)
        } else {
            workItem.perform()
        }
    }
}

这样,在我们的生产代码中,我们使用DispatchQueue.main.asyncTestable { ... }而不是DispatchQueue.main.async { ... }。在您的特定场景中:

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

并且您的测试应该在没有修改的情况下正确工作。

相关问题