Skip to content

Commit f1cd298

Browse files
committed
explain using an XCTestExpectation with a pipeline
1 parent e2dfe1e commit f1cd298

File tree

1 file changed

+68
-7
lines changed

1 file changed

+68
-7
lines changed

docs/pattern-test-pipeline-expectation.adoc

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,80 @@ __Goal__::
88
99
__References__::
1010

11-
* << link to reference pages>>
11+
* https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/DataTaskPublisherTests.swift[UsingCombineTests/DataTaskPublisherTests.swift]
12+
13+
* https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/EmptyPublisherTests.swift[UsingCombineTests/EmptyPublisherTests.swift]
14+
* https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/FuturePublisherTests.swift[UsingCombineTests/FuturePublisherTests.swift]
15+
* https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/PublisherTests.swift[UsingCombineTests/PublisherTests.swift]
16+
* https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/debounceAndRemoveDuplicatesPublisherTests.swift[UsingCombineTests/debounceAndRemoveDuplicatesPublisherTests.swift]
1217
1318
__See also__::
1419

15-
* << link to other patterns>>
20+
* <<#patterns-testing-subscriber>>
1621
1722
__Code and explanation__::
1823

19-
* set up an expectation (`XCTestExpectation`)
20-
* create your publisher & relevant pipeline if so desired
21-
* create a sink to capture the results that works on both completions and values
22-
** this can be separate, or just chained to the pipeline, depending on what makes most sense to you
23-
* `wait` on the expectation to let the test "do it's thing" in the background
24+
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.
25+
Combine, being driven by its subscribers, can set up a sync that initiates the data flow.
26+
You can use an https://developer.apple.com/documentation/xctest/xctestexpectation[XCTestExpectation] to wait an explicit amount of time for the test to run to completion.
27+
28+
A general pattern for using this with combine includes:
29+
30+
. set up the expectation within the test
31+
. establish the code you are going to test
32+
. set up the code to be invoked such that on the success path you call the expectation's `.fulfill()` function
33+
. set up a `wait()` function with an explicit timeout that will fail the test if the expectation isn't fulfilled within that time window.
34+
35+
If you are testing the data results from a pipeline, then triggering the `fulfill()` function within the <<reference.adoc#reference-sink>> operator `receiveValue` closure can be very convenient.
36+
If you are testing a failure condition from the pipeline, then often including `fulfill()` within the <<reference.adoc#reference-sink>> operator `receiveCompletion` closure is effective.
37+
38+
The following example shows testing a one-shot publisher (dataTaskPublisher in this case) using expectation, and expecting the data to flow without an error.
39+
40+
.https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/DataTaskPublisherTests.swift#L47[UsingCombineTests/DataTaskPublisherTests.swift - testDataTaskPublisher]
41+
[source, swift]
42+
----
43+
func testDataTaskPublisher() {
44+
// setup
45+
let expectation = XCTestExpectation(description: "Download from \(String(describing: testURL))") <1>
46+
let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: self.testURL!)
47+
// validate
48+
.sink(receiveCompletion: { fini in
49+
print(".sink() received the completion", String(describing: fini))
50+
switch fini {
51+
case .finished: expectation.fulfill() <2>
52+
case .failure: XCTFail() <3>
53+
}
54+
}, receiveValue: { (data, response) in
55+
guard let httpResponse = response as? HTTPURLResponse else {
56+
XCTFail("Unable to parse response an HTTPURLResponse")
57+
return
58+
}
59+
XCTAssertNotNil(data)
60+
// print(".sink() data received \(data)")
61+
XCTAssertNotNil(httpResponse)
62+
XCTAssertEqual(httpResponse.statusCode, 200) <4>
63+
// print(".sink() httpResponse received \(httpResponse)")
64+
})
65+
66+
XCTAssertNotNil(remoteDataPublisher)
67+
wait(for: [expectation], timeout: 5.0) <5>
68+
}
69+
----
70+
71+
<1> The expectation is set up with a string that makes debugging in the event of failure a bit easier.
72+
This string is really only seen when a test failure occurs.
73+
The code we are testing here is dataTaskPublisher retrieving data from a preset test URL, defined earlier in the test.
74+
The publisher is invoked by attaching the <<reference.adoc#reference-sink>> subscriber to it.
75+
Without the expectation, the code will still run, but the test running structure wouldn't wait to see if there were any exceptions.
76+
The expectation within the test "holds the test" waiting for a response to let the operators do their work.
77+
<2> In this case, the test is expected to complete successfully and terminate normally, therefore thethe `expectation.fulfill()` invocation is set within the receiveCompletion closure, specifically linked to a received `.finished` completion.
78+
<3> Since we don't expect a failure, we also have an explicit XCTFail() invocation if we receive a `.failure` completion.
79+
<4> We have a few additional assertions within the receiveValue.
80+
Since this publisher set returns a single value and then terminates, we can make easily make inline assertions about the data received.
81+
If we received multiple values, then we could collect those and make assertions on what was received after the fact.
82+
<5> This test uses a single expectation, but you can include multiple independent expectations to require fulfillment.
83+
It also sets that maximum time that this test can run to five seconds.
84+
The test will not always take five seconds, as it will complete the test as soon as the fulfill is received.
2485

2586
// force a page break - in HTML rendering is just a <HR>
2687
<<<

0 commit comments

Comments
 (0)