Swift 5.5 在初始化中测试异步任务
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)
}
Tnx to matt 这是正确的方法。测试函数中不需要异步,只需使用谓词即可完成工作。
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)
}
Expectation-and-wait 是正确的。你只是用错了。
你想多了。您不需要 async
测试方法。您不需要自己致电 fulfill
。您不需要 Combine 链。只需使用谓词期望等待 vm.result
设置。
基本上规则是这样的:测试 async
方法需要 async
测试方法。但是测试碰巧进行异步调用的方法的异步“结果”,例如您的 init
方法,只需要良好的 old-fashioned expectation-and-wait 测试。
我举个例子。这是您的代码的简化版本;该结构与您正在做的基本相同:
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)
}
}
专家额外内容: Bool 谓词期望是一种非常常用的东西,手头有一个结合期望的便捷方法会很有用, predicate,和wait合并成一个包:
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
}
我想测试一下我的 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)
}
Tnx to matt 这是正确的方法。测试函数中不需要异步,只需使用谓词即可完成工作。
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)
}
Expectation-and-wait 是正确的。你只是用错了。
你想多了。您不需要 async
测试方法。您不需要自己致电 fulfill
。您不需要 Combine 链。只需使用谓词期望等待 vm.result
设置。
基本上规则是这样的:测试 async
方法需要 async
测试方法。但是测试碰巧进行异步调用的方法的异步“结果”,例如您的 init
方法,只需要良好的 old-fashioned expectation-and-wait 测试。
我举个例子。这是您的代码的简化版本;该结构与您正在做的基本相同:
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)
}
}
专家额外内容: Bool 谓词期望是一种非常常用的东西,手头有一个结合期望的便捷方法会很有用, predicate,和wait合并成一个包:
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
}