Breaking Dependencies

Chuck Greb
Android Testing
Published in
5 min readSep 14, 2018
The Breakfast Machine by Arne Hendriks via Flickr Creative Commons

Dependencies are one of the biggest challenges to getting legacy code into a test harness. Especially dependencies on the Android framework.

In the Android world, having a dependency on an activity, fragment, or view is basically a non-starter for unit testing. Other types of dependencies that pose challenges to unit testing include networking calls, file I/O, database access, and globals, just to name a few.

Dependencies are ofter the most obvious impediment to testing. The two ways this problem manifests itself are difficulty instantiating objects in test harnesses and difficulty running methods in test harnesses. Often in legacy code, you have to break dependencies to get test in place.

Working Effectively with Legacy Code, Michael Feathers (2005)

Android Dependencies

In the previous post, we extracted a simple presenter class to allow us to unit test the presentation logic that was originally inline in a button click handler. The key here was breaking the dependency on the Android framework to allow us to unit test the logic in isolation.

Last time we were able to first add a few instrumentation tests as a bit of a safety net before refactoring. Unfortunately, is not always possible. Let’s look at another example, this time in Java so we can enjoy a more authentic legacy code experience.

UserPresenter.java

public class UserPresenter {
private final UserController controller;

private UserProfile user;

public UserPresenter(UserController controller) {
this.controller = controller;
}

public void onProfileButtonClick(Context context) {
if (user == null) {
user = DatabaseManager.getInstance().fetchUser(context);
}

controller.showProfile(user);
}
}

In this example, we have an app with a feature to display a user profile screen.

The good news is we already have a UserPresenter class decoupled from the Activity.

The bad news is the presenter depends on a DatabaseManager and a Context.

Even worse, the DatabaseManager is a singleton. Having a dependency on a global singleton can be particularly challenging when trying to get a class into a unit test harness.

Let’s also assume we can’t introduce instrumentation tests for this feature without pre-populating the database with user profile data and navigating a complex series of UI interactions. While this might sound interesting to you, I can think of better ways to spend my time. Remember we would have to do this every time we run the tests on an emulator or device.

And after all that, you still won’t have the ability to write unit tests that can verify the behavior of your presenter in isolation. As an alternative, we can use a dependency breaking technique.

Dependency Breaking Techniques

Dependency breaking techniques are minimally invasive refactors that can help make a system more testable.

Isn’t there risk when you start refactoring without having tests in place? Of course. However these techniques are designed so the chance of making a mistake is very small.

Breaking dependencies in legacy code will allow the introduction of tests into the system before making more invasive design improvements.

In the above example, we actually have a few different options for how to break the dependency of the UserPresenter on the DatabaseManager singleton and the Android Context.

  1. Parameterize Constructor
  2. Parameterize Method
  3. Introduce Static Setter

Parameterize Constructor

Rather than relying on the getInstance() method of the DatabaseManager singleton, we could pass an instance of the class into the constructor.

This would allow us to inject a fake or mock DatabaseManager in the test environment into the UserPresenter.

UserPresenter.java

public class UserPresenter {
private final UserController controller;
private final DatabaseManager databaseManager;

private UserProfile user;

public UserPresenter(UserController controller,
DatabaseManager databaseManager) {
this.controller = controller;
this.databaseManager = databaseManager;
}

public void onProfileButtonClick(Context context) {
if (user == null) {
user = databaseManager.fetchUser(context);
}

controller.showProfile(user);
}
}

Parameterize Method

We can perform a similar technique by introducing a new parameter to the onProfileButtonClick method.

UserProfile.java

public class UserPresenter {
private final UserController controller;

private UserProfile user;

public UserPresenter(UserController controller) {
this.controller = controller;
}

public void onProfileButtonClick(Context context,
DatabaseManager databaseManager) {
if (user == null) {
user = databaseManager.fetchUser(context);
}

controller.showProfile(user);
}
}

Introduce Static Setter

A third way we can decouple the dependency on the real DatabaseManager in tests is by introducing a static setter method that allows us to replace the real instance by a fake instance in the test environment.

Let’s say our DatabaseManager looks something like the following. For the sake of brevity we’ll omit the details of the database access.

DatabaseManager.java

public class DatabaseManager {
private static final DatabaseManager instance =
new DatabaseManager();

public static DatabaseManager getInstance() {
return instance;
}

private DatabaseManager() {
}

public UserProfile fetchUser(Context context) {
UserProfile user = new UserProfile();

// Fetch user profile data from SQLite database...

return user;
}
}

Introducing a static setter method that can be used in the test harness requires a few small steps.

  1. Relax constructor scope from private to protected so we can create a test subclass that avoids accessing the real database.
  2. Make the static instance mutable by removing the keyword final.
  3. Add a test-only method to set the instance in the test environment.

DatabaseManager.java

public class DatabaseManager {
private static DatabaseManager instance = new DatabaseManager();

public static DatabaseManager getInstance() {
return instance;
}

@VisibleForTesting
public static void setTestInstance(DatabaseManager instance) {
DatabaseManager.instance = instance;
}

protected DatabaseManager() {
}

public UserProfile fetchUser(Context context) {
UserProfile user = new UserProfile();

// Fetch user profile data from SQLite database...

return user;
}
}

Does this looks a little gross? Yes. When introducing tests in legacy code, sometimes the design of the program will get worse before it gets better.

Once we have tests in place, we can start to make more comprehensive design improvements. For example, perhaps we could eventually migrate the DatabaseManager from a singleton to a normal instance class.

Which one of these dependency breaking technique is best to use? This answer is individual to your program.

Parameterize Constructor and Parameterize Method are less invasive, but can lead to widespread changes in the code base if many classes have the same dependency.

Introduce Static Setter is a slightly more invasive technique, as it introduces “test only” code into the production class. Yet this can be a reasonable trade-off when there are many classes that depend on the same singleton instance.

In the book Working Effectively with Legacy Code, there are many more dependency breaking techniques. In the coming posts, we will look at even more of these small tactical refactors to facilitate testing, and how they can be applied to Android projects. See you next time!

This post is part of a series on working with legacy code on Android. It explores ways we can navigate, maintain, improve, and evolve legacy code using clean architecture, refactoring, dependency breaking techniques, and testing.

If you found this article helpful, please give it some applause 👏 to help others find it. You can also leave a comment below. Thanks!

--

--

Chuck Greb
Android Testing

Mission-driven engineering leader. Community organizer. Digital minimalist.