Refactoring for testability / Removing singletons

Removing singletons

Let’s check what can we do to improve code testability by removing singletons methods calls from methods.

Daniel Sumara

--

Photo by Caspar Camille Rubin on Unsplash

I don’t want to elaborate on is singletons are good, bad, useful, helps solve some problems or introduce ones. I would like to focus on how they affect code testability.

Code that heavily relies on singletons is commonly considered untestable. It’s very hard to verify our assumptions or fakes input data when we cannot use test double.

But this kind of code can be easily refactored in few steps to allow testability.

A short story about our assignment

Imagine our application can generate some kind of report. To do it application makes a network request. The amount of data is quite big, so this operation can take over one minute. To prevent screen lock and request cancellation we need to disable the idle timer. Let’s assume our network session timeouts are configured properly.

To improve user experience and fulfil his needs we need to add the ability to generate report between selected dates.

Iterations

Identifying what to do

We should start with identifying the code we need to amend. For my example, I decided to start with the model class which is responsible for communication with the backend service and application layer.

This class uses two singletons: UIApplication.shared and ReportsNetworkService.shared. I would consider this class untestable.

In terms of unit tests, I would like to test if

  • an idle timer is disabled when this method is called
  • an idle timer is enabled back again when the network service respond (either with success and failure)
  • failure error is passed to the client if the network service fails for some reason
  • valid GeneratedReport is passed to the client if the network service didn’t fail

From the above list, I’m able to check only the first position. The rest of the cases are dependent of ReportsNetworkService which make calls to third party service. We cannot predict the response from this service.

Considering writing unit tests for the above code:

  • we shouldn't rely on external dependencies (like backend service)
  • be able to provide the desired output from dependencies (like response result)

The first iteration of refactoring

The first and the easiest thing to do to make this class more testable is assigning singletons as object properties.
We can do this using constructor (initializer) injection like:

As part of refactoring activities:

  • we do not introduce changes outside class (thanks to initializer default parameters)
  • we improve code structure (we can identify class dependencies)
  • we improve code understanding (application property is used to interact with the application, reportsNetworkService is used to communicate with backend service) — do we?

Second iteration?

Changes we made doesn’t really help make this component more testable. In test target, we can create classes that inherit from our dependencies and override called methods to provide desired behaviour or check if the method was called, but in my opinion, it is not the way we should follow.

But at this point, we know exactly the dependency interface we are using. We can extract it and make our classes confirm it.

After this, we can update the class we are focused on to looks like:

We didn’t make any changes outside the class.
We didn’t introduce any changes in behaviour.
We did introduce new focused protocols for dependencies.
We did rename properties to be more meaning full.

At this point, we should finish refactoring activities for this component.

It is worth making a commit or even pull (or merge) request to let someone evaluate our changes.

What’s next?

The next step should be writing unit tests (as described above) to cover expected component behaviour.

As we have focused protocol on which system under test relays we can easily create test double in the test target.

When we finish with this activity we should again allow someone to review our code.

From that point, we are free to extend component functionality. Thanks to previous actions we have clear code which we understand. We have also unit tests that should prevent introducing regression.

--

--