I'm having difficulties testing Combine. I'm following:
Which tests:
final class ViewModel {
@Published private(set) var tokens = [String]()
@Published var string = ""
private let tokenizer = Tokenizer()
init () {
$string
.flatMap(tokenizer.tokenize)
.replaceError(with: [])
.assign(to: &$tokens)
}
}
struct Tokenizer {
func tokenize(_ string: String) -> AnyPublisher<[String], Error> {
let strs = string.components(separatedBy: " ")
return Just(strs)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
with the following:
func testTokenizingMultipleStrings() throws {
let viewModel = ViewModel()
let tokenPublisher = viewModel.$tokens
.dropFirst()
.collect(2)
.first()
viewModel.string = "Hello @john"
viewModel.string = "Check out #swift"
let tokenArrays = try awaitPublisher(tokenPublisher)
XCTAssertEqual(tokenArrays.count, 2)
XCTAssertEqual(tokenArrays.first, ["Hello", "john"])
XCTAssertEqual(tokenArrays.last, ["Check out", "swift"])
}
And the following helper function:
extension XCTestCase {
func awaitPublisher<T: Publisher>(
_ publisher: T,
timeout: TimeInterval = 10,
file: StaticString = #file,
line: UInt = #line
) throws -> T.Output {
var result: Result<T.Output, Error>?
let expectation = self.expectation(description: "Awaiting publisher")
let cancellable = publisher.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
result = .failure(error)
case .finished:
break
}
expectation.fulfill()
},
receiveValue: { value in
result = .success(value)
}
)
waitForExpectations(timeout: timeout)
cancellable.cancel()
let unwrappedResult = try XCTUnwrap(
result,
"Awaited publisher did not produce any output",
file: file,
line: line
)
return try unwrappedResult.get()
}
}
Here receiveValue is never called so the test doesn't complete.
How can I get this test to pass?
I ran into the same issue and eventually realized that for the test to pass, we need to update the view-model between setting up the subscription and waiting for the expectation. Since both currently happen inside the awaitPublisher helper, I added a closure parameter to that function:
func awaitPublisher<T: Publisher>(
_ publisher: T,
timeout: TimeInterval = 10,
file: StaticString = #file,
line: UInt = #line,
closure: () -> Void
) throws -> T.Output {
...
let expectation = ...
let cancellation = ...
closure()
waitForExpectations(timeout: timeout)
...
}
Note the exact position of the closure – it won’t work if it’s called too early or too late.
You can then call the helper in your test like so:
let tokenArrays = try awaitPublisher(publisher) {
viewModel.string = "Hello @john"
viewModel.string = "Check out #swift"
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With