[TUT] Mocking shared preferences to test in isolation

Below is an excerpt from my book “Learning Android Application Testing” that explains how you can write tests that interact with the Android system using files, databases or shared preferences without corrupting or being affected by the data that already exists.

It’s post Christmas and there is a flurry of consumerism whilst the shops do their January sales and we panick to buy everything! Well don’t worry you won’t miss the boat as “Learning Android Application Testing” is on sale until January 8th for £4 / $5 right here:

https://www.packtpub.com/application-development/learning-android-application-testing

Learning Android Application Testing / Baking with testing recipes

We expect that the tests that alter these preferences’ values will not affect the behavior of the real application. Without the correct testing framework, the tests could delete user account information for an application that stores these values as shared preferences. This doesn’t sound like a good idea. So what we really need is the ability to mock a Context that also mocks the access to SharedPreferences. Our first attempt could be to use RenamingDelegatingContext, but unfortunately, it does not mock SharedPreferences, although it is close because it mocks the database and file system access. So first, we need to mock access to our shared preferences.

Whenever you come across a new class (like RenamingDelegatingContext), it’s a good idea to read the relevant Java doc to get an overview of how the framework developers expect it to be used. For more information, refer to https://developer.android.com/reference/android/test/RenamingDelegatingContext.html.

Let’s create the specialized Context. The RenamingDelegatingContext class is a very good point to start from because as we mentioned before, database and file system access will be mocked. The problem is how to mock the SharedPreferences access Remember that RenamingDelegatingContext, as its name suggests, delegates everything to a Context. So the root of our problem lies in this Context.

When you access SharedPreferences from a Context, you use getSharedPreferences(String name, int mode). To change the way this method works, we can override it inside RenamingMockContext. Now that we have control, we can prepend the name parameter with our test prefix, which means that when our tests run, they will write to a preferences file that is different than that of our main application:

public class RenamingMockContext extends RenamingDelegatingContext {

 private static final String PREFIX = "test.";

 public RenamingMockContext(Context context) {

  super(context, PREFIX);

 }

 @Override

 public SharedPreferences getSharedPreferences(String name, int mode) {

  return super.getSharedPreferences(PREFIX + name, mode);

 }

}

Now, we have full control over how preferences, databases, and files are stored.

Mocking contexts

We have the RenamingMockContext class. Now, we need a test that uses it. As we will be testing an application, the base class for the test would be ApplicationTestCase. This test case provides a framework in which you can test application classes in a controlled environment. It provides basic support for the lifecycle of an application, and hooks to inject various dependencies and control the environment in which your application is tested. Using the setContext() method, we can inject the RenamingMockContext method before the application is created.

We’re going to test an application called TemperatureConverter. This is a simple application that converts Celsius to Fahrenheit and vice versa. We will discuss more about the development of this app in Chapter 6, Test-driven Development. For now, the details aren’t necessary as we are concentrating on testing scenarios.

The TemperatureConverter application will store the decimal places of any conversion as a shared preference. Consequently, we will create a test to set the decimal places and then retrieve it to verify its value:

public class TemperatureConverterApplicationTests extends ApplicationTestCase<TemperatureConverterApplication> {

 public TemperatureConverterApplicationTests() {

  this("TemperatureConverterApplicationTests");

 }

 public TemperatureConverterApplicationTests(String name) {

  super(TemperatureConverterApplication.class);

  setName(name);

 }

 public void testSetAndRetreiveDecimalPlaces() {

  RenamingMockContext mockContext = new RenamingMockContext(getContext());

  setContext(mockContext);

  createApplication();

  TemperatureConverterApplication application = getApplication();

  application.setDecimalPlaces(3);

  assertEquals(3, application.getDecimalPlaces());

 }

}

We extend ApplicationTestCase using the TemperatureConverterApplication template parameter. Then, we use the given name constructor pattern that we discussed in Chapter 2, Understanding testing with the Android SDK.

Here, we have not used a setUp() method since there is only one test in the class–you ain’t gonna need it as they say. One day, if you come to add another test to this class, this is when you can override setUp() and move the behavior. This follows the DRY principle, meaning Don’t Repeat Yourself, and leads to more maintainable software. At the top of the test method, we create the mock context and set the context for this test using the setContext() method; we create the application using createApplication(). You need to ensure you call setContext before createApplication as this is how you get the correct instantiation order.

Now, the code that actually tests for the required behavior setting the decimal places, retrieving it, and verifying its value. This is it, using RenamingMockContext to give us control over SharedPreferences. Whenever the SharedPreference is requested, the method will invoke the delegating context, adding the prefix for the name. The original SharedPreferences class used by the application are unchanged:

public class TemperatureConverterApplication extends Application {

 private static final int DECIMAL_PLACES_DEFAULT = 2;

 private static final String KEY_DECIMAL_PLACES = ".KEY_DECIMAL_PLACES";

 private SharedPreferences sharedPreferences;

 @Override

 public void onCreate() {

  super.onCreate();

  sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

 }

 public void setDecimalPlaces(int places) {
 
  Editor editor = sharedPreferences.edit();

  editor.putInt(KEY_DECIMAL_PLACES, places);

  editor.apply();

 }

 public int getDecimalPlaces() {

  return sharedPreferences.getInt(KEY_DECIMAL_PLACES, DECIMAL_PLACES_DEFAULT);

 }

}

We can verify that our tests do not affect the application by furnishing the TemperatureConverterApplication class with some value in the shared preferences, running the application, then running the tests and eventually verifying that this value was not affected by executing the tests.

Hope you found that interesting, there’s a lot more recipes and testing insight in the book, so take advantage of the offer above. Enjoy the rest of the holidays!

Learning Android Application Testing