Swift 5.5 test async Task in init



我想测试我的init函数是否按预期工作。在Task{}块的init中有一个异步调用。如何让我的测试等待任务块的结果?

class ViewModel: ObservableObject {
@Published private(set) var result: [Item]

init(fetching: RemoteFetching) {
self.result = []
Task {
do {
let result = try await fetching.fetch()

self.result = result // <- need to do something with @MainActor?
} catch {
print(error)   
}
}
}  
}

测试:

func testFetching() async {
let items = [Item(), Item()]
let fakeFetching = FakeFetching(returnValue: items)
let vm = ViewModel(fetching: FakeFetching())

XCTAssertEqual(vm.result, [])

// wait for fetching, but how?

XCTAssertEqual(vm.result, items])
}

我尝试了这个,但是设置项目,只发生在XCTWaiter之后。编译器警告不能用await调用XCTWaiter,因为它不是异步的。

func testFetching() async {
let items = [Item(), Item()]
let fakeFetching = FakeFetching(returnValue: items)
let expectation = XCTestExpectation()
let vm = ViewModel(fetching: FakeFetching())

XCTAssertEqual(vm.result, [])

vm.$items
.dropFirst()
.sink { value in
XCTAssertEqual(value, items)
expectation.fulfill()
}
.store(in: &cancellables)

let result = await XCTWaiter.wait(for: [expectation], timeout: 1)

XCTAssertEqual(result, .completed)
}

期待和等待是正确的。你只是用错了。

你想得太多了。您不需要async测试方法。您不需要自己调用fulfill。你不需要一个联合收割机链。只需使用谓词期望等待,直到vm.result设置。

基本上规则是这样的:测试async方法需要async测试方法。但是测试异步的"结果"对于碰巧进行异步调用的方法,如init方法,只需进行老式的期望和等待测试。

我举个例子。这是你的代码的简化版本;结构基本上和你所做的是一样的:

protocol Fetching {
func fetch() async -> String
}
class MyClass {
var result = ""
init(fetcher: Fetching) {
Task {
self.result = await fetcher.fetch()
}
}
}

好了,下面是测试方法:

final class MockFetcher: Fetching {
func fetch() async -> String { "howdy" }
}
final class MyLibraryTests: XCTestCase {
let fetcher = MockFetcher()
func testMyClassInit() {
let subject = MyClass(fetcher: fetcher)
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate(block: { _, _ in
subject.result == "howdy"
}), object: nil
)
wait(for: [expectation], timeout: 2)
}
}

Extra for experts:Bool谓词期望是一种非常常用的东西,因此如果有一个方便的方法将期望、谓词和等待合并到一个包中,将会很有用:

extension XCTestCase {
func wait(
_ condition: @escaping @autoclosure () -> (Bool),
timeout: TimeInterval = 10)
{
wait(for: [XCTNSPredicateExpectation(
predicate: NSPredicate(block: { _, _ in condition() }), object: nil
)], timeout: timeout)
}
}
结果是,例如,上面的测试代码可以简化为:
func testMyClassInit() {
let subject = MyClass(fetcher: fetcher)
wait(subject.result == "howdy")
}

确实方便。在我自己的代码中,我经常添加一个显式的断言,,即使它是完全多余的,只是为了让它完全清楚我声称我的代码做什么:

func testMyClassInit() {
let subject = MyClass(fetcher: fetcher)
wait(subject.result == "howdy")
XCTAssertEqual(subject.result, "howdy") // redundant but nice
}

这是正确的方法。不需要在测试函数中使用async,只需使用谓词就可以完成任务。

func testFetching() {
let items = [Item(), Item()]
let fakeFetching = FakeFetching(returnValue: items)
let expectation = XCTestExpectation()
let vm = ViewModel(fetching: FakeFetching())

let pred = NSPredicate { _, _ in
vm.items == items
}
let expectation = XCTNSPredicateExpectation(predicate: pred, object: vm)

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

在Matt精彩的回答上略有变化。在我的例子中,为了额外的方便,我把他的扩展方法分解成更细粒度的扩展。

<<p>辅助框架/strong>
public typealias Predicate = () -> Bool
public extension NSPredicate {
convenience init(predicate: @escaping @autoclosure Predicate) {
self.init{ _, _ in predicate() }
}
}
public extension XCTNSPredicateExpectation {
convenience init(predicate: @escaping @autoclosure Predicate, object: Any) {
self.init(predicate: NSPredicate(predicate: predicate()), object: object)
}
convenience init(predicate: @escaping @autoclosure Predicate) {
self.init(predicate: NSPredicate(predicate: predicate()))
}
convenience init(predicate: NSPredicate) {
self.init(predicate: predicate, object: nil)
}
}
public extension XCTestCase {
func XCTWait(for condition: @escaping @autoclosure Predicate, timeout: TimeInterval = 10) {
let expectation = XCTNSPredicateExpectation(predicate: condition())
wait(for: [expectation], timeout: timeout)
}
}

有了上面的内容,OP的代码可以简化为如下…

func testFetching() {
let items = [Item(), Item()]
let fakeFetching = FakeFetching(returnValue: items)
let vm = ViewModel(fetching: FakeFetching())

XCTWait(for: vm.items == items, timeout: 1)   
}

命名说明

上面,我在调用函数XCTWait时使用了一个有点争议的名称。这是因为XCT前缀应该被认为是为苹果的XCTest框架保留的。然而,决定以这种方式命名它源于提高其可发现性的愿望。通过这样命名它,当开发人员在代码编辑器中输入XCT时,XCTWait现在作为提供的自动完成项之一呈现**,使查找和使用更有可能。

然而,一些纯粹主义者可能不喜欢这种方法,理由是如果苹果添加了类似的命名,这些代码可能会突然中断/停止工作(尽管不太可能,除非签名也匹配)

因此,请自行决定使用这些命名。或者,只需将其重命名为您喜欢的/符合您自己的命名标准的名称。

(**如果它在同一个项目中或者在他们导入的库/包中)

最新更新