- Goal
-
-
For testing a pipeline, or subscriber, when what you want to test is the timing of the pipeline.
-
- References
- See also
- Code and explanation
-
There are a number of operators in Combine that are specific to the timing of data, including debounce, throttle, and delay. You may want to test that your pipeline timing is having the desired impact, independently of doing UI testing.
One way of handling this leverages the both XCTestExpectation and a passthroughSubject, combining the two.
Building on both Testing a publisher with XCTestExpectation and Testing a subscriber with a PassthroughSubject, add DispatchQueue in the test to schedule invocations of PassthroughSubject’s .send()
method.
An example of this:
func testKVOPublisher() {
let expectation = XCTestExpectation(description: self.debugDescription)
let foo = KVOAbleNSObject()
let q = DispatchQueue(label: self.debugDescription) (1)
let _ = foo.publisher(for: \.intValue)
.print()
.sink { someValue in
print("value of intValue updated to: >>\(someValue)<<")
}
q.asyncAfter(deadline: .now() + 0.5, execute: { (2)
print("Updating to foo.intValue on background queue")
foo.intValue = 5
expectation.fulfill() (3)
})
wait(for: [expectation], timeout: 5.0) (4)
}
-
This adds a
DispatchQueue
to your test, naming the queue after the test itself. This really only shows when debugging test failures, and is convenient as a reminder of what is happening in the test code vs. any other background queues that might be in use. -
.asyncAfter
is used along with the deadline parameter to define when a call gets made. -
The simplest form embeds any relevant assertions into the subscriber or around the subscriber. Additionally, invoking the
.fulfill()
on your expectation as the last queued entry you send lets the test know that it is now complete. -
Make sure that when you set up the wait that allow for sufficient time for your queue’d calls to be invoked.
A definite downside to this technique is that it forces the test to take a minimum amount of time matching the maximum queue delay in the test.
Another option is a 3rd party library named EntwineTest, which was inspired by the RxTest library. EntwineTest is part of Entwine, a Swift library that expands on Combine with some helpers. The library can be found on Github at https://github.com/tcldr/Entwine.git, available under the MIT license.
One of the key elements included in EntwineTest is a virtual time scheduler, as well as additional classes that schedule (TestablePublisher
) and collect and record (TestableSubscriber
) the timing of results while using this scheduler.
An example of this from the EntwineTest project README is included:
func testExampleUsingVirtualTimeScheduler() {
let scheduler = TestScheduler(initialClock: 0) (1)
var didSink = false
let cancellable = Just(1) (2)
.delay(for: 1, scheduler: scheduler)
.sink { _ in
didSink = true
}
XCTAssertNotNil(cancellable)
// where a real scheduler would have triggered when .sink() was invoked
// the virtual time scheduler requires resume() to commence and runs to
// completion.
scheduler.resume() (3)
XCTAssertTrue(didSink) (4)
}
-
Using the virtual time scheduler requires you create one at the start of the test, initializing its clock to a starting value. The virtual time scheduler in EntwineTest will commence subscription at the value
200
and times out at900
if the pipeline isn’t complete by that time. -
You create your pipeline, along with any publishers or subscribers, as normal. EntwineTest also offers a testable publisher and a testable subscriber that could be used as well. For more details on these parts of EntwineTest, see Using EntwineTest to create a testable publisher and subscriber.
-
.resume()
needs to be invoked on the virtual time scheduler to commence its operation and run the pipeline. -
Assert against expected end results after the pipeline has run to completion.