Improve the Testability of Your Android App
The most effective way to improve the testability of your Android app is through a technique such as dependency injection. Dependency injection on Android is a notoriously difficult problem to tackle. The problem stems from the fact that Android manages the creation and destruction of all activity instances, thereby making it difficult for us to provide dependencies. The diagram below, taken from the Android developer docs, illustrates the activity lifecycle:
So why is this an issue? When building testable apps, we want to be able to inject dependencies into our activities. This allows us to swap out our "real" objects with testable mocks. However, we can't do this since we don't have control over the creation of our activities. Unfortunately, this tends to promote a design involving heavy use of static singletons. Let's take a look at an example of a typical Android activity class.
Note: I'm not going to go into detail over what dependency injection and inversion of control (IoC) is. It has become a fairly common design pattern in the past few years, and there are plenty of other resources that can explain it far better than I can.
Basic Activity Example
/**
* A simple activity that makes a web service call when a button is clicked.
*/
public class MainActivity extends Activity {
private WebServiceClient client;
@Override
public void onCreate(final Bundle savedInstanceState) {
client = WebServiceClient.getInstance();
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
client.doSomething();
}
});
}
}
This makes sense at a first glance, but now say we want to test our activity. How do we do this without making real web service calls? We can create an integration test that makes calls to our server, but those are slow, less reliable, and require a separate testing environment.
Also notice that there is no straightforward way of providing objects to the onCreate()
method. We cannot add any additional parameters because it is an overridden method defined by the base Activity
class. We also cannot create a constructor because the Android OS only knows how to call the default empty constructor. So how do we fix this?
Using The Service Locator Pattern
The first option is to use the service locator pattern. If you aren't familiar with this pattern, it's basically a way for our activity to grab the services (or dependencies) it needs.
The basic idea behind a service locator is to have an object that knows how to get hold of all of the services that an application might need.
Inversion of Control Containers and the Dependency Injection Pattern by Martin Fowler
The following is an example of how this might work in the context of an Android app.
Android Service Locator
public class MainActivity extends Activity {
private WebServiceClient client;
@Override
public void onCreate(final Bundle savedInstanceState) {
client = ServiceLocator.getServiceLocator().getWebServiceClient();
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
client.doSomething();
}
});
}
}
public class ServiceLocator {
private static ServiceLocator instance;
private IWebServiceClient webServiceClient;
public static void setServiceLocator(ServiceLocator serviceLocator) {
this.instance = serviceLocator;
}
public static ServiceLocator getServiceLocator() {
return this.serviceLocator;
}
public ServiceLocator(IWebServiceClient webServiceClient) {
this.webServiceClient = webServiceClient;
}
public IWebServiceClient getWebServiceClient() {
return this.webServiceClient;
}
}
Now we can provide our ServiceLocator
with a mock WebServiceClient
when writing tests.
Testing With Service Locator
public class MainActivityTests {
@Test
public void testClickingButtonDoesSomethingWithWebService() {
IWebServiceClient mockWebService = createMock(WebServiceClient.class);
ws.doSomething();
expectLastCall();
replay(mockWebService);
ServiceLocator sl = new ServiceLocator(mockWebService);
verify(mockWebService);
}
}
Our code is now much better. We can easily test logic that makes web service calls, our dependencies are clearly defined through ServiceLocator
, and we've reduced the coupling between classes.
Using Dagger
The nice part about using the service locator pattern is that it can work nicely with regular dependency injection. Dagger is a lightweight IoC framework that was designed with Android in mind. Dagger can automatically handle our dependencies, alleviating a lot of the effort.
The standard way of using an IoC framework, like Dagger, is to resolve all of your dependencies at startup. However, on Android this isn't possible because we cannot instantiate activities ourself. Instead, we can use Dagger's ObjectGraph
as a type of service locator.
Dagger Example
public class MainActivity extends Activity {
@Inject
IWebServiceClient client;
@Override
public void onCreate(final Bundle savedInstanceState) {
MyApp app = (MyApp) getApplication();
app.getObjectGraph().inject(this);
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
client.doSomething();
}
});
}
}
@Module(entryPoints = MainActivity.class)
public class DefaultModule {
@Provides IWebServiceClient provideWebServiceClient() {
return new WebServiceClient();
}
}
public class MyApp extends Application {
private ObjectGraph objectGraph;
@Override
public void onCreate() {
super.onCreate();
this.objectGraph = ObjectGraph.create(new DefaultModule());
}
public ObjectGraph getObjectGraph() {
return this.objectGraph;
}
}
So there we have it. I won't go into the details of how to use Dagger, but if you've used an IoC framework before, the syntax is fairly self-explanatory. Here, we get an instance of Dagger's object graph and use it to inject dependencies into our MainActivity
object. Our Dagger ObjectGraph
instance is essentially acting as a service locator.
Advantages
Using IoC provides a few advantages over our manual service locator:
- All of our dependencies are grouped together. Instead of having calls to
ServiceLocator
sprinkled throughout our code, our injected field variables are conveniently listed in a single place. - Reduced boiler plate. Manually injecting dependencies can become cumbersome as our app becomes more complex. This leads to the next point...
- Developers are more likely to use it. When your design is easier to implement, other team members are more likely to stick to it. It also means that less time is spent passing around dependencies and figuring out the order in which to instantiate them.
- Using Dagger also allows us to use regular dependency injection. Activities need to use a service locator because of their lifecycle, but other objects do not have this limitation. With Dagger we can satisfy as many dependencies as possible at the start of our app, but fall back to using a service locator pattern with our activities.
Summary
As mobile devices become more powerful, the apps we create become more complex. It's important to take the time to ensure your Android apps are designed in a modular, testable way. It is generally agreed that a lack of automated testing is no longer acceptable for desktop and web apps, and mobile shouldn't be any exception.