- Goal
-
-
For testing a publisher (and any pipeline attached)
-
- References
- See also
- Code and explanation
-
When you are testing a publisher, or something that creates a publisher, you may not have the option of controlling when the publisher returns data for your tests. Combine, being driven by its subscribers, can set up a sync that initiates the data flow. You can use an XCTestExpectation to wait an explicit amount of time for the test to run to completion.
A general pattern for using this with Combine includes:
-
set up the expectation within the test
-
establish the code you are going to test
-
set up the code to be invoked such that on the success path you call the expectation’s
.fulfill()
function -
set up a
wait()
function with an explicit timeout that will fail the test if the expectation isn’t fulfilled within that time window.
If you are testing the data results from a pipeline, then triggering the fulfill()
function within the reference.adoc operator receiveValue
closure can be very convenient.
If you are testing a failure condition from the pipeline, then often including fulfill()
within the reference.adoc operator receiveCompletion
closure is effective.
The following example shows testing a one-shot publisher (URLSession.dataTaskPublisher in this case) using expectation, and expecting the data to flow without an error.
func testDataTaskPublisher() {
// setup
let expectation = XCTestExpectation(description: "Download from \(String(describing: testURL))") (1)
let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: self.testURL!)
// validate
.sink(receiveCompletion: { fini in
print(".sink() received the completion", String(describing: fini))
switch fini {
case .finished: expectation.fulfill() (2)
case .failure: XCTFail() (3)
}
}, receiveValue: { (data, response) in
guard let httpResponse = response as? HTTPURLResponse else {
XCTFail("Unable to parse response an HTTPURLResponse")
return
}
XCTAssertNotNil(data)
// print(".sink() data received \(data)")
XCTAssertNotNil(httpResponse)
XCTAssertEqual(httpResponse.statusCode, 200) (4)
// print(".sink() httpResponse received \(httpResponse)")
})
XCTAssertNotNil(remoteDataPublisher)
wait(for: [expectation], timeout: 5.0) (5)
}
-
The expectation is set up with a string that makes debugging in the event of failure a bit easier. This string is really only seen when a test failure occurs. The code we are testing here is
dataTaskPublisher
retrieving data from a preset test URL, defined earlier in the test. The publisher is invoked by attaching the sink subscriber to it. Without the expectation, the code will still run, but the test running structure wouldn’t wait to see if there were any exceptions. The expectation within the test "holds the test" waiting for a response to let the operators do their work. -
In this case, the test is expected to complete successfully and terminate normally, therefore the
expectation.fulfill()
invocation is set within the receiveCompletion closure, specifically linked to a received.finished
completion. -
Since we don’t expect a failure, we also have an explicit
XCTFail()
invocation if we receive a.failure
completion. -
We have a few additional assertions within the receiveValue. Since this publisher set returns a single value and then terminates, we can make inline assertions about the data received. If we received multiple values, then we could collect those and make assertions on what was received after the fact.
-
This test uses a single expectation, but you can include multiple independent expectations to require fulfillment. It also sets that maximum time that this test can run to five seconds. The test will not always take five seconds, as it will complete the test as soon as the fulfill is received. If for some reason the test takes longer than five seconds to respond, the XCTest will report a test failure.