我正在使用 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() 或任意延迟的正确方法是什么?理想情况下,我希望测试在执行断言之前等待任务完成,但没有硬编码的睡眠持续时间或黑客让它工作。在这种情况下有更好的测试模式的建议吗?
当您发现编写测试很困难时,这可能暗示您的代码设计存在问题。
在这种情况下,您的交互器中有一个函数
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)
}
}