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.
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745633264a4637234.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