Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock sharedpreferences for android instrumentation tests?

I have a preference util class to store and retrieve the data in shared preferences in a single place.

Prefutils.java:

public class PrefUtils {
  private static final String PREF_ORGANIZATION = "organization";

  private static SharedPreferences getPrefs(Context context) {
    return PreferenceManager.getDefaultSharedPreferences(context);
  }

  private static SharedPreferences.Editor getEditor(Context context) {
    return getPrefs(context).edit();
  }

  public static void storeOrganization(@NonNull Context context,
      @NonNull Organization organization) {
    String json = new Gson().toJson(organization);
    getEditor(context).putString(PREF_ORGANIZATION, json).apply();
  }

  @Nullable public static Organization getOrganization(@NonNull Context context) {
    String json = getPrefs(context).getString(PREF_ORGANIZATION, null);
    return new Gson().fromJson(json, Organization.class);
  }
}

Sample code showing PrefUtils usage in LoginActivity.java:

@Override public void showLoginView() {
    Organization organization = PrefUtils.getOrganization(mActivity);
    mOrganizationNameTextView.setText(organization.getName());
  }

List of androidTestCompile dependencies in build.gradle:

// Espresso UI Testing dependencies.
  androidTestCompile "com.android.support.test.espresso:espresso-core:$project.ext.espressoVersion"
  androidTestCompile "com.android.support.test.espresso:espresso-contrib:$project.ext.espressoVersion"
  androidTestCompile "com.android.support.test.espresso:espresso-intents:$project.ext.espressoVersion"

  androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
  androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2:'

src/androidTest/../LoginScreenTest.java

@RunWith(AndroidJUnit4.class) @LargeTest public class LoginScreenTest {

@Rule public ActivityTestRule<LoginActivity> mActivityTestRule =
      new ActivityTestRule<>(LoginActivity.class);

  @Before public void setUp() throws Exception {
    when(PrefUtils.getOrganization(any()))
          .thenReturn(HelperUtils.getFakeOrganization());
  } 
}

The above code to return fakeOrganization was not working, running the tests on login activity results in NullPointerException in line mOrganizationNameTextView.setText(organization.getName()); defined in the above LoginActivity.java class.

How to solve the above issue?

like image 943
blizzard Avatar asked Jul 01 '16 21:07

blizzard


2 Answers

Approach-1:

Expose SharedPreference with application scope using Dagger2 and use it like @Inject SharedPreferences mPreferences in activity/fragment.

Sample code using the above approach to save(write) a custom preference:

SharedPreferences.Editor editor = mPreferences.edit();
    editor.putString(PREF_ORGANIZATION, mGson.toJson(organization));
    editor.apply();

To read a custom preference:

 String organizationString = mPreferences.getString(PREF_ORGANIZATION, null);
    if (organizationString != null) {
      return mGson.fromJson(organizationString, Organization.class);
    }

If you use it like above it results in breaking the DRY principle, since the code will be repeated in multiple places.


Approach-2:

This approach is based on the idea of having a separate preference class like StringPreference/ BooleanPreference which provides wrapper around the SharedPreferences code to save and retrieve values.

Read the below posts for detailed idea before proceeding with the solution:

  1. Persist your data elegantly: U2020 way by @tasomaniac
  2. Espresso 2.1: ActivityTestRule by chiuki
  3. Dagger 2 + Espresso 2 + Mockito

Code:

ApplicationModule.java

@Module public class ApplicationModule {
  private final MyApplication mApplication;

  public ApplicationModule(MyApplication application) {
    mApplication = application;
  }

  @Provides @Singleton public Application provideApplication() {
    return mApplication;
  }
}

DataModule.java

@Module(includes = ApplicationModule.class) public class DataModule {

  @Provides @Singleton public SharedPreferences provideSharedPreferences(Application app) {
    return PreferenceManager.getDefaultSharedPreferences(app);
  }
}

GsonModule.java

@Module public class GsonModule {
  @Provides @Singleton public Gson provideGson() {
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    return gsonBuilder.create();
  }
}

ApplicationComponent.java

@Singleton @Component(
    modules = {
        ApplicationModule.class, DataModule.class, GsonModule.class
    }) public interface ApplicationComponent {
  Application getMyApplication();
  SharedPreferences getSharedPreferences();
  Gson getGson();
}

MyApplication.java

public class MyApplication extends Application {
  @Override public void onCreate() {
    initializeInjector();
  }

   protected void initializeInjector() {
    mApplicationComponent = DaggerApplicationComponent.builder()
        .applicationModule(new ApplicationModule(this))
        .build();
  }
}

OrganizationPreference.java

public class OrganizationPreference {

  public static final String PREF_ORGANIZATION = "pref_organization";

  SharedPreferences mPreferences;
  Gson mGson;

  @Inject public OrganizationPreference(SharedPreferences preferences, Gson gson) {
    mPreferences = preferences;
    mGson = gson;
  }

  @Nullable public Organization getOrganization() {
    String organizationString = mPreferences.getString(PREF_ORGANIZATION, null);
    if (organizationString != null) {
      return mGson.fromJson(organizationString, Organization.class);
    }
    return null;
  }

  public void saveOrganization(Organization organization) {
    SharedPreferences.Editor editor = mPreferences.edit();
    editor.putString(PREF_ORGANIZATION, mGson.toJson(organization));
    editor.apply();
  }
}

Wherever you need the preference just inject it using Dagger @Inject OrganizationPreference mOrganizationPreference;.

For androidTest, I'm overriding the preference with a mock preference. Below is my configuration for android tests:

TestDataModule.java

public class TestDataModule extends DataModule {

  @Override public SharedPreferences provideSharedPreferences(Application app) {
    return Mockito.mock(SharedPreferences.class);
  }
}

MockApplication.java

public class MockApplication extends MyApplication {
  @Override protected void initializeInjector() {
    mApplicationComponent = DaggerTestApplicationComponent.builder()
        .applicationModule(new TestApplicationModule(this))
        .dataModule(new TestDataModule())
        .build();
  }
}

LoginScreenTest.java

@RunWith(AndroidJUnit4.class) public class LoginScreenTest {

@Rule public ActivityTestRule<LoginActivity> mActivityTestRule =
      new ActivityTestRule<>(LoginActivity.class, true, false);

  @Inject SharedPreferences mSharedPreferences;
  @Inject Gson mGson;

 @Before public void setUp() {
    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();

    MyApplication app = (MyApplication) instrumentation.getTargetContext().getApplicationContext();
    TestApplicationComponent component = (TestApplicationComponent) app.getAppComponent();
    component.inject(this);
    when(mSharedPreferences.getString(eq(OrganizationPreference.PREF_ORGANIZATION),
        anyString())).thenReturn(mGson.toJson(HelperUtils.getFakeOrganization()));

    mActivityTestRule.launchActivity(new Intent());
  }
}

Make sure you have dexmaker mockito added in build.gradle

androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2:'
like image 147
blizzard Avatar answered Nov 15 '22 04:11

blizzard


Unfortunately Mockito cannot perform what you are looking for on its own. You have two options, one is to use Power Mock and the other is to change Prefutils into a normal class and instead use a Dependency Injection Framework.

Power Mock

Nice and simple, this will let you mock static methods, check out this SO post for details. On the downside it may result in other issues based on the comments in that SO post.

Dependency Injection Approach (my original answer)

You are trying to write a UI test with some of the behavior of the application "mocked". Mockito is built to let you write Unit tests where you test a specific object (or group of objects) and mock some of their behavior.

You can see some examples of how mockito is used in these tests (1, 2). None of them test the UI, instead they instantiate an object "stub"/"mock" some if its behavior and then test the rest.

To achieve what you want you will instead need a dependency injection framework. This allows you to change the "implementation" of some of your application based on whether you are running the actual application or a test.

The details of how you mock behavior of your classes/objects varies from framework to framework. This blog post goes over how to use Dagger 2 with Mockito and espresso you can apply the same approach for your tests. It also has links to presentations that give more background on dagger 2.

If you don't like dagger 2 then you can also checkout RoboGuice and Dagger. Just note, I do not think butter-knife will fit your needs as it doesn't support injection of Pojos.

like image 24
Dr. Nitpick Avatar answered Nov 15 '22 04:11

Dr. Nitpick