Reputation: 1479
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?
Upvotes: 3
Views: 3559
Reputation: 422
I unfortunately didn't find the answers from the responses above helpful. Later, I discovered this Swift Package, and it elegantly addressed our pain points. This author save me a day.
Here are my example test function.
import Combine
import TestableCombinePublishers
import XCTest
func testCollect3Values() {
let values = [0, 1, 2]
let intValue = CurrentValueSubject<Int, Never>(-1)
intValue.send(values[0])
let test = intValue
.collect(values.count) // 3
.expect(values) // [0, 1, 2]
.expectNoCompletion()
intValue.send(values[1])
intValue.send(values[2])
test.waitForExpectations(timeout: 1)
}
Upvotes: 0
Reputation: 171
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"
}
Upvotes: 2
Reputation: 23701
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)
Your tokenPublisher
won't do anything until you subscribe to it. In this code you create the publisher, do some actions that would have pushed values through the Publisher if anyone was subscribed to it, then you call awaitPublisher
(the thing that does the subscription). You need to reverse those:
let viewModel = ViewModel()
let tokenPublisher = viewModel.$tokens
.dropFirst()
.collect(2)
.first()
let tokenArrays = try awaitPublisher(tokenPublisher)
viewModel.string = "Hello @john"
viewModel.string = "Check out #swift"
Upvotes: 0