Hot questions for Using Mockito in android instrumentation

Top 10 Java Open Source / Mockito / android instrumentation

Question:

I want to spy the Linkedlist in android.

List list = new LinkedList();
List spyData = Mockito.spy(list);
spyData.add("xxxx");

However, the exception occured.

java.lang.AbstractMethodError: abstract method "boolean org.mockito.internal.invocation.AbstractAwareMethod.isAbstract()"
    at     org.mockito.internal.invocation.InvocationImpl.callRealMethod(InvocationImpl.java:109)
    at org.mockito.internal.stubbing.answers.CallsRealMethods.answer(CallsRealMethods.java:41)
    at org.mockito.internal.handler.MockHandlerImpl.handle(MockHandlerImpl.java:93)
    at org.mockito.internal.handler.NullResultGuardian.handle(NullResultGuardian.java:29)
    at org.mockito.internal.handler.InvocationNotifierHandler.handle(InvocationNotifierHandler.java:38)
    at com.google.dexmaker.mockito.InvocationHandlerAdapter.invoke(InvocationHandlerAdapter.java:49)
    at LinkedList_Proxy.add(LinkedList_Proxy.generated)
    at com.app.test.testmethod(mytest.java:202)
    at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:191)
    at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:176)
    at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:555)
    at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1858)

The dependencies of libs are

dexmaker-1.2.jar
dexmaker-mockito-1.2.jar
mockito-core-1.10.19.jar

Even I update mockito-core-1.10.19.jar to mockito-core-2.0.31-beta.jar,

the problem still exists.

But Mockito.mock(Linkedlist.class) is ok, I have no ideas about this problem.

Thank you.


Answer:

I just found another way to solve the problem.

This is an issue for dexmaker 1.2, we should upgrade to dexmaker 1.4, dexmaker-mockito 1.4 and include dexmaker-dx-1.4.

So the dependencies are

dexmaker-dx-1.4.jar
dexmaker-1.4.jar
dexmaker-mockito-1.4.jar
mockito-core-1.10.19.jar

Question:

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?


Answer:

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:'

Question:

I try to mock a method in my instrumentation test but it fails and I am looking for a solution to solve it.

public class MyTest extends InstrumentationTestCase {

    private Context mAppCtx;

    @Override
    public void setUp() throws Exception {
        super.setUp();

        Context context = mock(Context.class);

        mAppCtx = getInstrumentation().getContext().getApplicationContext();                

        when(mAppCtx.createPackageContext(PACKAGE_NAME, 0)).thenReturn(context);

    }

A crash happens on the following line:

when(mAppCtx.createPackageContext(PACKAGE_NAME, 0)).thenReturn(context);

And I got following error:

org.mockito.exceptions.misusing.MissingMethodInvocationException:
when() requires an argument which has to be 'a method call on a mock'.
For example:
when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
Those methods *cannot* be stubbed/verified.
2. inside when() you don't call method on mock but on some other object.
3. the parent of the mocked class is not public.
It is a limitation of the mock engine.

Answer:

You need to mock each method invocation of: getInstrumentation().getContext().getApplicationContext();

Example:

Instrumentation inst = mock(Instrumentation.class);
Context instContext = mock(Context.class);
ApplicationContext mAppCtx= mock(ApplicationContext.class);
when(getInstrumentation()).thenReturn(inst);
when(inst.getContext()).thenReturn(instContext);
when(instContext.getApplicationContext()).thenReturn(mAppCtx);
when(mAppCtx.createPackageContext(PACKAGE_NAME, 0)).thenReturn(context);