XCTestExpectation subclasses that simplify testing Combine Publishers and help to improve the readability of unit tests.
Writing tests for Combine Publishers (or @Published properties) using XCTestExpectation
usually involves some boilerplate code such as:
let expectation = XCTestExpectation(description: "Wait for the publisher to emit the expected value")
viewModel.$output.sink { _ in
} receiveValue: { value in
if value == expectedValue {
expectation.fulfill()
}
}
.store(in: &cancellables)
wait(for: [expectation], timeout: 1)
We can try using a XCTKeyPathExpectation
but it requires that the observed object inherits from NSObject
and also marking the properties we want to observe with both the @objc
attribute and the dynamic
modifier to make them KVO-compliant:
class ViewModel: NSObject {
@objc dynamic var isLoading = false
}
let expectation = XCTKeyPathExpectation(keyPath: \ViewModel.isLoading, observedObject: viewModel, expectedValue: true)
Another tempting approach would be using XCTNSPredicateExpectation
like:
let expectation = XCTNSPredicateExpectation(predicate: NSPredicate { _,_ in
viewModel.output == expectedValue
}, object: viewModel)
While this looks nice and compact, the problem with XCTNSPredicateExpectation
is that is quite slow and best suited for UI tests. This is because it uses some kind of polling mechanism that adds a significant delay of 1 second minimum before the expectation is fulfilled. So it's better not to follow this path in unit tests.
The PublisherExpectations is a set of 3 XCTestExpectation that allows declaring expectations for publisher events in a clear and concise manner. They inherit from XCTestExpectation so they can be used in the wait(for: [expectations])
call as with any other expectation.
PublisherValueExpectation
: An expectation that is fulfilled when a publisher emits a value that matches a certain condition.PublisherFinishedExpectation
: An expectation that is fulfilled when a publisher completes successfully.PublisherFailureExpectation
: An expectation that is fulfilled when a publisher completes with a failure.let publisherExpectation = PublisherValueExpectation(stringPublisher, expectedValue: "Got it")
let publisherExpectation = PublisherValueExpectation(arrayPublisher) { $0.contains(value) }
@Published
property wrappers as well:let publisherExpectation = PublisherValueExpectation(viewModel.$isLoaded, expectedValue: true)
let publisherExpectation = PublisherValueExpectation(viewModel.$keywords) { $0.contains("Cool") }
let publisherExpectation = PublisherFinishedExpectation(publisher)
let publisherExpectation = PublisherFinishedExpectation(publisher, expectedValue: 2)
let publisherExpectation = PublisherFinishedExpectation(arrayPublisher) { array in
array.allSatisfy { $0 > 5 }
}
let publisherExpectation = PublisherFailureExpectation(publisher)
let publisherExpectation = PublisherFailureExpectation(publisher) { error in
guard case .apiError(let code) = error, code = 500 else { return false }
return true
}
let publisherExpectation = PublisherFailureExpectation(publisher, expectedError: ApiError(code: 100))
Thanks to Combine we can adapt the publisher to check many things while keeping the test readability:
let publisherExpectation = PublisherValueExpectation(publisher.collect(3), expectedValue: [1,2,3])
let publisherExpectation = PublisherValueExpectation(publisher.first(), expectedValue: 1)
let publisherExpectation = PublisherValueExpectation(publisher.last(), expectedValue: 5)
let publisherExpectation = PublisherValueExpectation(publisher.dropFirst().first(), expectedValue: 2)
link |
Stars: 3 |
Last commit: 3 weeks ago |
First release
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics