如何在 XCTest 中使用 Swift 中的异步任务测试 Interactor 方法?

问题描述 投票:0回答:1

我正在使用 VIP(Clean Swift)架构在 Swift 中开发一个应用程序。我有一个交互器,它有一个由 ViewController 的 viewDidLoad (为了方便起见也称为 viewDidLoad)触发的方法,它启动一个异步任务来获取数据,然后更新 UI。 UI 更新是通过 Presenter 完成的(被注入到交互器中)。

在我的测试中,我想验证当交互者的viewDidLoad被触发时,演示者的方法被调用。我正在使用模拟演示者并检查是否调用了预期的方法。但是,必须等待调用演示者的方法,因为它们使用

@MainActor
运行,并且我不确定如何在测试中等待它们。

这是我的交互器的简化版本:

protocol MyInteractorProtocol {
    func viewDidLoad()
}

class MyInteractor: MyInteractorProtocol {
    private var presenter: MyPresenterProtocol

    init(presenter: MyPresenterProtocol) {
        self.presenter = presenter
    }

    func viewDidLoad() {
        Task {
            await fetchData()
        }
    }

    private func fetchData() async {
        // Some data fetching here
        await updateUI()
    }

    @MainActor
    private func updateUI() {
        presenter.presentData()
    }
}

我当前的测试:

import XCTest

final class MyInteractorTests: XCTestCase {
    func testViewDidLoadCallsPresenterUsingSleep() async throws {
        let mockPresenter = MockPresenter()
        let interactor = MyInteractor(presenter: mockPresenter)

        interactor.viewDidLoad()

        // I'm currently using sleep to wait for the async task to finish
        try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second

        XCTAssertTrue(mockPresenter.presentDataCalled)
    }

    func testViewDidLoadCallsPresenterUsingExpectation() {
        let mockPresenter = MockPresenter()
        let interactor = MyInteractor(presenter: mockPresenter)

        let expectation = expectation(description: "Presenter should call presentData")

        Task {
            interactor.viewDidLoad()
            expectation.fulfill()
        }

        wait(for: [expectation])
        XCTAssertTrue(mockPresenter.presentDataCalled)
    }
}

第一个测试使用

Task.sleep()
等待异步操作完成,但我想避免这种方法。在第二个测试中,我使用了 XCTestExpectation,但感觉很hacky(并且似乎会导致不稳定的测试)。

问题:在 Swift XCTest 中测试异步任务而不依赖 Task.sleep() 或任意延迟的正确方法是什么?理想情况下,我希望测试在执行断言之前等待任务完成,但没有硬编码的睡眠持续时间或黑客让它工作。在这种情况下有更好的测试模式的建议吗?

ios swift unit-testing testing concurrency
1个回答
0
投票

当您发现编写测试很困难时,这可能暗示您的代码设计存在问题。

在这种情况下,您的交互器中有一个函数

viewDidLoad
,它异步完成工作,但不向其调用者提供任何指示该工作已完成。事实上,只需查看该函数的签名,您就会认为它是同步完成的。

我会更改您的设计,以便将

viewDidLoad
明确标记为
async
。这可以让您从其实现中消除非结构化的
Task
,向调用者表明它异步执行工作,并为他们提供一种了解工作何时完成的方法。

当然,您仍然需要在视图控制器中使用

Task
viewDidLoad
,因为您无法实现该功能
async

我也不会直接从

fetchData
更新 UI - 将这两个操作分开。

class MyInteractor: MyInteractorProtocol {
    private var presenter: MyPresenterProtocol

    init(presenter: MyPresenterProtocol) {
        self.presenter = presenter
    }

    func viewDidLoad() async throws {
        let someData = try await fetchData()
        await updateUI(with:someData)  
    }

    private func fetchData() async throws -> SomeDataObject {
        // Some data fetching here
        return some data
    }

    @MainActor
    private func updateUI(with someData:SomeDataObject) {
        presenter.presentData(someData)
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.