admin管理员组文章数量:1026170
Consider this XCTest-based unit test:
func testImageRetrieved() {
let expectation = XCTestExpectation()
let cancellable = viewModel.$image.dropFirst().sink { _ in
// Do nothing on completion.
} receiveValue: {
XCTAssertNotNil($0)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
cancellable.cancel()
}
According to Apple's Migrating a test from XCTest, this should be directly translatable into this Swift Testing-based method:
@Test
func imageRetrieved() async {
var cancellable: AnyCancellable?
await confirmation { imageRetrieved in
cancellable = viewModel.$image.dropFirst().sink { _ in
// Do nothing on completion.
} receiveValue: {
#expect($0 != nil)
imageRetrieved()
}
}
cancellable?.cancel()
}
The latter test, however, fails with the error saying: "Confirmation was confirmed 0 times, but expected to be confirmed 1 time." It looks like the confirmation doesn't "wait" until the publisher emits a value.
What is the proper way to test Combine publishers in Swift Testing?
Consider this XCTest-based unit test:
func testImageRetrieved() {
let expectation = XCTestExpectation()
let cancellable = viewModel.$image.dropFirst().sink { _ in
// Do nothing on completion.
} receiveValue: {
XCTAssertNotNil($0)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
cancellable.cancel()
}
According to Apple's Migrating a test from XCTest, this should be directly translatable into this Swift Testing-based method:
@Test
func imageRetrieved() async {
var cancellable: AnyCancellable?
await confirmation { imageRetrieved in
cancellable = viewModel.$image.dropFirst().sink { _ in
// Do nothing on completion.
} receiveValue: {
#expect($0 != nil)
imageRetrieved()
}
}
cancellable?.cancel()
}
The latter test, however, fails with the error saying: "Confirmation was confirmed 0 times, but expected to be confirmed 1 time." It looks like the confirmation doesn't "wait" until the publisher emits a value.
What is the proper way to test Combine publishers in Swift Testing?
Share Improve this question edited Nov 18, 2024 at 7:56 lazarevzubov asked Nov 17, 2024 at 13:30 lazarevzubovlazarevzubov 2,3423 gold badges18 silver badges32 bronze badges 3 |2 Answers
Reset to default 6You are correct, that “confirmation
doesn’t ‘wait’ until the publisher emits a value.” As that document says:
Confirmations function similarly to the expectations API of
XCTest
, however, they don’t block or suspend the caller while waiting for a condition to be fulfilled. Instead, the requirement is expected to be confirmed (the equivalent of fulfilling an expectation) beforeconfirmation()
returns, and records an issue otherwise.
If you look at the confirmation
examples in that doc, they all await
some async
call before returning.
But you can use values
to render your Publisher
as an AsyncSequence
, which you can simply await
in an async
test. So rather than sink
, just get the first value from this asynchronous sequence:
@Test
func publisherValue() async {
let publisher = …
let value = await publisher.values.first()
#expect(value != nil)
}
So, in your case, you should be able to do:
@Test
func imageRetrieved() async {
let value = await viewModel.$image.values.first()
#expect(value != nil)
}
With this extension to simplify the call to first(where:)
:
extension AsyncSequence {
func first() async rethrows -> Element? {
try await first(where: { _ in true})
}
}
It looks like the confirmation doesn't "wait" until the publisher emits a value.
Correct. the documentation says,
When the closure returns, the testing library checks if the confirmation’s preconditions have been met, and records an issue if they have not.
The closure in this case returns almost immediately, since all it does is assign to a variable. The test will not see any values that are published asynchronously.
Compare this to the example in the migration guide.
struct FoodTruckTests {
@Test func truckEvents() async {
await confirmation("…") { soldFood in
FoodTruck.shared.eventHandler = { event in
if case .soldFood = event {
soldFood()
}
}
await Customer().buy(.soup)
}
...
}
...
}
At the end of the closure there, await Customer().buy(.soup)
is called, and this is presumably what will trigger FoodTruck.shared.eventHandler
.
For publishers, you can easily get its values
as an AsyncSequence
, which is much easier to consume. For example, to check that the publisher publishes at least one element, and that the first element is not nil. you can do:
@Test func someTest() async throws {
var iterator = publisher.values.makeAsyncIterator()
let first = await iterator.next()
#expect(first != nil)
}
// example publisher:
var publisher: some Publisher<Int?, Never> {
Just<Int?>(1).delay(for: 1, scheduler: DispatchQueue.main)
}
Also consider adding a timeout
before .values
.
Alternatively, you can manually wait by just calling Task.sleep
. The following tests that publisher
publishes exactly one element, which is not nil, in a 2 second period.
@Test func someTest() async throws {
var cancellable: Set<AnyCancellable> = []
try await confirmation { confirmation in
publisher.sink { value in
#expect(first != nil)
confirmation()
}.store(in: &cancellable)
try await Task.sleep(for: .seconds(2))
}
}
Of course, this makes the test run for at least 2 seconds, which might be undesirable sometimes.
Consider this XCTest-based unit test:
func testImageRetrieved() {
let expectation = XCTestExpectation()
let cancellable = viewModel.$image.dropFirst().sink { _ in
// Do nothing on completion.
} receiveValue: {
XCTAssertNotNil($0)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
cancellable.cancel()
}
According to Apple's Migrating a test from XCTest, this should be directly translatable into this Swift Testing-based method:
@Test
func imageRetrieved() async {
var cancellable: AnyCancellable?
await confirmation { imageRetrieved in
cancellable = viewModel.$image.dropFirst().sink { _ in
// Do nothing on completion.
} receiveValue: {
#expect($0 != nil)
imageRetrieved()
}
}
cancellable?.cancel()
}
The latter test, however, fails with the error saying: "Confirmation was confirmed 0 times, but expected to be confirmed 1 time." It looks like the confirmation doesn't "wait" until the publisher emits a value.
What is the proper way to test Combine publishers in Swift Testing?
Consider this XCTest-based unit test:
func testImageRetrieved() {
let expectation = XCTestExpectation()
let cancellable = viewModel.$image.dropFirst().sink { _ in
// Do nothing on completion.
} receiveValue: {
XCTAssertNotNil($0)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
cancellable.cancel()
}
According to Apple's Migrating a test from XCTest, this should be directly translatable into this Swift Testing-based method:
@Test
func imageRetrieved() async {
var cancellable: AnyCancellable?
await confirmation { imageRetrieved in
cancellable = viewModel.$image.dropFirst().sink { _ in
// Do nothing on completion.
} receiveValue: {
#expect($0 != nil)
imageRetrieved()
}
}
cancellable?.cancel()
}
The latter test, however, fails with the error saying: "Confirmation was confirmed 0 times, but expected to be confirmed 1 time." It looks like the confirmation doesn't "wait" until the publisher emits a value.
What is the proper way to test Combine publishers in Swift Testing?
Share Improve this question edited Nov 18, 2024 at 7:56 lazarevzubov asked Nov 17, 2024 at 13:30 lazarevzubovlazarevzubov 2,3423 gold badges18 silver badges32 bronze badges 3-
The example you gave is quite a bad/contrived example. Assuming
image
is@Published
, the test (unlike what you claimed) passes. This is because@Published
properties always publish their current value synchronously when you first subscribe to them. Presumably you want to confirm some value that will be published asynchronously. How about usingJust<Int?>(1).delay(for: 1, scheduler: DispatchQueue.main)
as an example instead? – Sweeper Commented Nov 17, 2024 at 16:43 -
@Sweeper You're right, I wanted to simplify code for the example and accidentally made it unrealistic. The real code has also
dropFirst()
, which ignores the initial value—added to the example. Now the code is exactly as in the real project I'm working on. – lazarevzubov Commented Nov 18, 2024 at 7:58 - Okay. Both Rob’s answer and my answer should still stand. – Sweeper Commented Nov 18, 2024 at 8:06
2 Answers
Reset to default 6You are correct, that “confirmation
doesn’t ‘wait’ until the publisher emits a value.” As that document says:
Confirmations function similarly to the expectations API of
XCTest
, however, they don’t block or suspend the caller while waiting for a condition to be fulfilled. Instead, the requirement is expected to be confirmed (the equivalent of fulfilling an expectation) beforeconfirmation()
returns, and records an issue otherwise.
If you look at the confirmation
examples in that doc, they all await
some async
call before returning.
But you can use values
to render your Publisher
as an AsyncSequence
, which you can simply await
in an async
test. So rather than sink
, just get the first value from this asynchronous sequence:
@Test
func publisherValue() async {
let publisher = …
let value = await publisher.values.first()
#expect(value != nil)
}
So, in your case, you should be able to do:
@Test
func imageRetrieved() async {
let value = await viewModel.$image.values.first()
#expect(value != nil)
}
With this extension to simplify the call to first(where:)
:
extension AsyncSequence {
func first() async rethrows -> Element? {
try await first(where: { _ in true})
}
}
It looks like the confirmation doesn't "wait" until the publisher emits a value.
Correct. the documentation says,
When the closure returns, the testing library checks if the confirmation’s preconditions have been met, and records an issue if they have not.
The closure in this case returns almost immediately, since all it does is assign to a variable. The test will not see any values that are published asynchronously.
Compare this to the example in the migration guide.
struct FoodTruckTests {
@Test func truckEvents() async {
await confirmation("…") { soldFood in
FoodTruck.shared.eventHandler = { event in
if case .soldFood = event {
soldFood()
}
}
await Customer().buy(.soup)
}
...
}
...
}
At the end of the closure there, await Customer().buy(.soup)
is called, and this is presumably what will trigger FoodTruck.shared.eventHandler
.
For publishers, you can easily get its values
as an AsyncSequence
, which is much easier to consume. For example, to check that the publisher publishes at least one element, and that the first element is not nil. you can do:
@Test func someTest() async throws {
var iterator = publisher.values.makeAsyncIterator()
let first = await iterator.next()
#expect(first != nil)
}
// example publisher:
var publisher: some Publisher<Int?, Never> {
Just<Int?>(1).delay(for: 1, scheduler: DispatchQueue.main)
}
Also consider adding a timeout
before .values
.
Alternatively, you can manually wait by just calling Task.sleep
. The following tests that publisher
publishes exactly one element, which is not nil, in a 2 second period.
@Test func someTest() async throws {
var cancellable: Set<AnyCancellable> = []
try await confirmation { confirmation in
publisher.sink { value in
#expect(first != nil)
confirmation()
}.store(in: &cancellable)
try await Task.sleep(for: .seconds(2))
}
}
Of course, this makes the test run for at least 2 seconds, which might be undesirable sometimes.
本文标签: How to test Combine Publishers in Swift TestingStack Overflow
版权声明:本文标题:How to test Combine Publishers in Swift Testing? - Stack Overflow 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://it.en369.cn/questions/1745633263a2160289.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
image
is@Published
, the test (unlike what you claimed) passes. This is because@Published
properties always publish their current value synchronously when you first subscribe to them. Presumably you want to confirm some value that will be published asynchronously. How about usingJust<Int?>(1).delay(for: 1, scheduler: DispatchQueue.main)
as an example instead? – Sweeper Commented Nov 17, 2024 at 16:43dropFirst()
, which ignores the initial value—added to the example. Now the code is exactly as in the real project I'm working on. – lazarevzubov Commented Nov 18, 2024 at 7:58