XCTest Debounce Effect in TCA — part 1

mein
3 min readSep 3, 2023

The testability in TCA is one of my favorite part. It is especially helpful when applying to the async scenerio.

https://rxmarbles.com/#debounce

There is a common async scenario like this, a continuous input and a discrete output via a timer. For example, when the user is typing some search phrase continuously, the app may not want to make the API request continuously accordingly, instead, it would like to make the API request in a more reasonable discrete way.

We know the debounce effect could be applied to this scenario. However does it really work as expected? We would know it only after we write some tests.

The testability of TCA makes it transparent to see whether any intermediate state changed or not. And whether any action triggered or not.

Besides, by dependency injection, TCA provides DispatchQueue.test to replace MainQueue at test time. It provides a controllable timing whenever the scheduler is used.

I will demo three use cases:

  • Debounce Independent Request without Intermediate State Changing
  • Debounce Queued Request with Intermediate State Changing
  • Debounce Async Request

Debounce Independent Request

First, make a TCA reducer to handle the request and response.

And then the XCTest code.

It is easy to test some varieties, too.

func test2()async throws{
let debounceDuration : DispatchQueue.SchedulerTimeType.Stride = 1
let testQueue = DispatchQueue.test
let store = TestStore(initialState: State(debounceDuration:debounceDuration)) {
IndependentRequestReducer()
} withDependencies: {
$0.mainQueue = testQueue.eraseToAnyScheduler()
}
await store.send(.debounceIndependentRequest(request: "A"))
await testQueue.advance(by: debounceDuration + 0.5 )
await store.receive(.response(request: "A" ), timeout: .zero)
{
$0.response = "A"
}
await store.send(.debounceIndependentRequest(request: "B"))
await testQueue.advance(by: debounceDuration - 0.5 )
await store.send(.debounceIndependentRequest(request: "C"))
await testQueue.advance(by: debounceDuration + 0.5 )
await store.receive(.response(request: "C" ), timeout: .zero)
{
$0.response = "C"
}
}

Debounce Queued Request

We could keep the queued requests in a state.

We then test the state changed every time accordingly, while the response action is only triggered at certain time.

Debounce Async Request

I will write this in part 2 of the article. It will be more close to the real world scenario when we make API request. So the request will be the async way. And there will be a dependency injection for the API client.

Noted, there is only one final async request at the end. On the contrary, if there are multiple async requests in the unit test, it is another story. Please refer to the Point-Free episodes #238 ~ #242

--

--