How to Write Unit Tests for Combine’s Debounce Method in iOS | by Arek Pituła | Jul, 2022

Testing is simple. Writing a testable code isn’t.

Writing testable code might be onerous. Writing testable code that requires asynchronous work is more durable.

A big proportion of purposes that implement mix use it largely to assist concurrent items of code, slightly than synchronous. Clearly, adopting a Mix operation to assist each async and sync operations is feasible, however in my sincere opinion utilizing a Mix to carry out a piece that isn’t associated to any of the next:

  • Execute a background work
  • Mix a number of sources
  • Async init

It’s a waste of readability.

On this article, I want to inform you how one can correctly check debounce technique, with a view to keep away from an extended code execution and flaky assessments.

Let’s begin with debounce and let me briefly remind you what this technique is and the definition:

Based on Apple documentation:

Publishes parts solely after a specified time interval elapses between occasions.

Basically, debounce will pause for a specified time after every worth is obtained, after which on the finish of that pause it’s going to ship the final worth.

When can or not it’s used? Mainly everytime you attempt to restrict a lot of ship occasions, and never each occasion is essential, slightly, the final results of particular sequence of occasions is essential to know. The most typical case is to restrict the variety of requests to the API when a person enters a question.

In a hypothetical scenario, let’s say a person is on the lookout for a selected product(iPhone) and the request is shipped to an API after every keystroke. The diagram under reveals how every request is shipped to the server. As you may even see, in naive implementation each letter triggers a brand new API request.

Sending a request that didn’t actually matter is inefficient and also can result in inconsistency. For instance, within the case the place one of many earlier requests was executed earlier than the final one. Clearly, you’ll be able to cancel each request earlier than executing the subsequent one. However it brings a brand new a part of code into the codebase, that must be examined and maintained. Right here is the place debounce involves our assist.

By giving legitimate parameters you might be positive that creating a brand new request is delayed till the scheduled time has elapsed. Within the instance above, with a delay time of about 0.5s which is sufficient for many customers to kind the subsequent char, just one request will likely be created: Request — (IPHONE). Clearly, if the person has a sluggish typing velocity, the request will likely be created a number of occasions, and it’s the developer’s duty to cancel it earlier than working the subsequent one.

Now let’s make it proper, guaranteeing testability and abstraction degree, that makes out code testable, readable, and maintainable.

Begin with a diagram, that describes what I wish to create:

Each consumer that has an entry to SearchStream is ready to subscribe to look outcomes and obtain the latest information from it. Customers can also search a selected question utilizing a search technique. Every time consumer(person) runs a search operate, the searched string is handed to the SearchStream, which raises a writer that handed searched values again to the consumer.

Let’s make two assumptions that will likely be key to scripting this module:

  1. SearchStream must be testable in synchronous assessments, utilizing a XCTest.waitForExpectations will likely be not wanted.
  2. The consumer is ready to ignore a debounce technique and count on a direct end result.

In the beginning let’s begin with the protocol, that describes a SearchStream:

  • associatedtype ResponseType — kind that’s returned
  • var searchResult: AnyPublisher<ResponseType, By no means> — stream which publishes subsequent results of looking out
  • func search(_ question: String) — technique run after each textual content change

Making a SearchStream protocol not a category causes that code to have one other abstraction layer and might be tailored to completely different necessities. For instance, one stream can return values from the backend, and the second can search inside the database for beforehand saved outcomes.

Once more, let’s check out the debounce technique definition:

Throughout testing there are two essential issues:

scheduler: It’s a protocol that defines when and how one can execute a closure. We’d like a two of them:

  • RunLoop.important — that is the run loop on the primary thread, it will likely be used on manufacturing code to obtain values on important thread.
  • ImmediateScheduler — particular scheduler, that performs synchronous actions.

Setting a scheduler correctly permits to satisfy the primary assumption:

SearchStream must be testable in synchronous assessments, utilizing a XCTest.waitForExpectations will likely be not wanted.

dueTime: Specify how a lot time to attend earlier than publishing a component.

Setting this worth to 0.0 to satisfy the second assumption:

Consumer is ready to ignore a debounce technique and count on a end result instantly

To deal with these parameters, let’s wrap up it right into a helpful struct:

It’s a good suggestion so as to add an extension, supporting entry to every of the aforementioned schedulers.

That is an instance how what a pattern SearchStream can seem like. I wish to carry your consideration to how the constructor is designed, and the way simple is to check it.

The simplest class to check is a category that haven’t any exterior dependency. It’s very not often, largely courses that assist primary logical operation, like string modification, mathematical equation, information rework, or related.

The truth is that almost all courses has exterior dependencies, like community, file system or different inner logic. This additionally works for SearchStream class.

Diagram of the SearchStream dependency

And here’s what the implementation seems to be like for each manufacturing and check targets.

Though the SearchStream is identical class, dependencies are completely different, which lets you management how a SearchStream ought to behave and set anticipated outcomes.

The check code might seem like this:

  • Due to ImmediateScheduler each check case is unbiased, there is no such thing as a Expectation or waitForExpectations code which are typically added to assist concurrency.
  • As a result of APISearchStream has injected dependency check case can also be capable of confirm whether or not search stream executes technique inside injected code.

Let’s sum up what’s most essential from this text:

  • Attempt to keep away from a waitForExpectations technique, it may decelerate your assessments. The extra ready in your check code, the longer it’s a must to anticipate all of the check circumstances, and also you’re placing pointless pressure on CI.
  • At all times attempt to break up your code into separate, weakly dependent components, which makes your code smaller, simpler, and testable.

More Posts