XCTest Debounce Effect in TCA — part 2

mein
2 min readSep 24, 2023

Debounce Async Request

The debounce effect was covered in part 1. This article will focus on:

  • implement the effect of async task in TCA
  • make a generic async task client in order to integrate with OpenAPI generator endpoints
  • using TCA's dependency injection for the async task client

Implement the effect of async task in TCA

Referring to TCA's sample code, FactClient, we create a reducer to handle the async task in the most basic form.

Here is the unit test.

Make a generic async task client in order to integrate with OpenAPI generator endpoints

Considering in the real world, the async task is most likely a http API request. And how about an OpenAPI client?

In WWDC 2023, Apple introduces Swift OpenAPI generator. It generates the endpoints of type Sendable. We can make a generic async task client in order to integrate with it. (I will write an OpenAPI generator integration example in another article.)

Here, the static passThroughClient and the fetchArrayClient work for the dependency injection in the unit test in next section.

Using TCA’s dependency injection for the async task client

First, we need to make the AsyncRequestClient comply with TCA’s DependencyKey and DependencyValues.

(Referring to TCA’s sample code, FactClient.)

Second, we include the dependency in the reducer. And we also make the request and response as generic type in order to integrate the OpenAPI generator endpoints in the future.

Then we can test how the dependency injection works. By default, TCA will use the testValue of the dependency in all tests. In our case, it is the passThroughClient.

We can override the dependency to use the fetchArrayClient.

func testArrayFetch()async throws{
typealias Reducer = AsyncRequestReducer<String,[String]>
typealias State = Reducer.State

let store = TestStore(initialState: State(debounceDuration:debounceDuration))
{
Reducer()
} withDependencies: {
$0.mainQueue = testQueue.eraseToAnyScheduler()
$0.asyncRequestClient = AsyncRequestClient.fetchArrayClient
}

for char in testString {
await store.send(.debounceQueuedRequest(request: "\(char)"))
await testQueue.advance(by: debounceDuration - 0.5 )
}
await testQueue.advance(by: debounceDuration + 0.5 )
await store.receive(.response(.success(["\(testString.last!)"]))
, timeout: .zero)
{ [self] in
$0.response = ["\(testString.last!)"]
}
}

All the examples above are showing the same result. That is, only the last async call has been called.

--

--