'Unit Testing Combine
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?
Solution 1:[1]
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"
Solution 2:[2]
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"
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Martin |