'Android Instrumentation test using Mockito: mocking not working

I am trying to run an Instrumentation test (androidTest) using Mockito in Android with Java.

The object under test is a simple fragment with and edittext in the layout. The edittext is loaded from a shared preference value. This is the relevant code:

public class KMCountFragment extends Fragment {

static final String SHARED_PREF_FILE = "cartrip_sharedpref";
static final String PREF_KEY_START_KM = "pref_start_km";


private SharedPreferences sharedPreferences;

private int startKMCount;
private int endKMCount;


@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    sharedPreferences = getContext().getSharedPreferences(SHARED_PREF_FILE, MODE_PRIVATE);

    startKMCount = sharedPreferences.getInt(PREF_KEY_START_KM, 0);     
}


public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
    ...
    binding.startKMCount.setText(String.valueOf(startKMCount));
}

This is the test:

@MediumTest
@RunWith(MockitoJUnitRunner.class)
public class KMCountFragmentIT {

    @Mock
    Context context;

    @Mock
    SharedPreferences sharedPrefs;


    @Before
    public void setUp() {
        sharedPrefs = mock(SharedPreferences.class);
        context = mock(Context.class);

        when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs);
    }

    @Test
    public void testFragmentCountersInitStata() {
        when(sharedPrefs.getInt(anyString(), anyInt())).thenReturn(20);

        FragmentScenario<KMCountFragment> scenario = FragmentScenario.launchInContainer(KMCountFragment.class);
        scenario.moveToState(Lifecycle.State.RESUMED);

        onView(withId(R.id.startKMCount)).check(matches(withText("20")));
    }
}

These are my dependencies in build.gradle:

dependencies {

    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.navigation:navigation-fragment:2.3.5'
    implementation 'androidx.navigation:navigation-ui:2.3.5'
    implementation files('libs/activation.jar')
    implementation files('libs/additionnal.jar')
    implementation files('libs/mail.jar')
    implementation 'androidx.preference:preference:1.1.1'
    implementation 'androidx.security:security-crypto:1.0.0'
    testImplementation 'junit:junit:4.13.2'

    testImplementation 'org.mockito:mockito-core:4.3.1'
    testImplementation 'androidx.test.ext:junit:1.1.3'

    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "org.mockito:mockito-android:4.3.1"
    androidTestImplementation "androidx.test:core:1.4.0"

    debugImplementation "androidx.fragment:fragment-testing:1.4.1"
}

The test keeps failing because the actual value in my app prefs is 999 and the test expects 20. The real context from the app is used and not the mocked one. I tried to mock the whole context also but same result:

androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: 'an instance of android.widget.TextView and view.getText() with or without transformation to match: is "20"' doesn't match the selected view.
Expected: an instance of android.widget.TextView and view.getText() with or without transformation to match: is "20"
Got: view.getText() was "999" transformed text was "999"

It seems like the mocking from Mockito is not working. Maybe I'm missing something stupid.. but I can't figure it out!



Solution 1:[1]

Your mocked Context will not be used when Espresso spawns your activity/fragment. Essentially, you cannot (or rather should not) mock Context in Android. I can think of three ways you can perform this test scenario.

  1. Use ApplicationProvider.getApplicationContext() to access application context in your tests and create a real SharedPreferences instance using it. When using this approach, do not forget to clear the SharedPreferences when tearing down the test (in the @After method).
  2. Inject SharedPreferences in Fragment after it has been created, by passing a callback to FragmentScenario#onFragment(). You can inject a mocked SharedPreferences this way, but this approach tends to be more smelly than the former one. Personally, I wouldn't recommend it unless you absolutely know what you're doing.
  3. Use a DI framework for injecting dependencies into Android components, e.g. Hilt. This is a neater approach, but it requires much effort. So it might not be worthwhile to do it for smaller projects.

Solution 2:[2]

OK, so this is my test. Not so clever, but it works in my case:

@MediumTest
@RunWith(MockitoJUnitRunner.class)
public class KMCountFragmentIT {

    private static final int START_KM_COUNT_TEST_VALUE = 10;
    private static final int END_KM_COUNT_TEST_VALUE = 90;

    private SharedPreferences sharedPreferences;

    private int startKMCount;
    private int endKMCount;
    private boolean clearPref;

    @Before
    public void setUp() {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();

        sharedPreferences = appContext.getSharedPreferences(SHARED_PREF_FILE, MODE_PRIVATE);

        if (sharedPreferences.contains(PREF_KEY_START_KM)) {
            clearPref = false;
            startKMCount = sharedPreferences.getInt(PREF_KEY_START_KM, 0);
        } else {
            clearPref = true;
            startKMCount = START_KM_COUNT_TEST_VALUE;
            SharedPreferences.Editor preferencesEditor = sharedPreferences.edit();
            preferencesEditor.putInt(PREF_KEY_START_KM, startKMCount);
            preferencesEditor.apply();
        }
    }

    @After
    public void tearDown() {
        if (clearPref) {
            sharedPreferences.edit().clear().apply();
        }
    }

    @Test
    public void testFragmentCountersInitState() {
        FragmentScenario<KMCountFragment> scenario = FragmentScenario.launchInContainer(KMCountFragment.class);
        scenario.moveToState(Lifecycle.State.RESUMED);

        onView(withId(R.id.startKMCount)).check(matches(withText(String.valueOf(startKMCount))));
    }
}

In the setup I check if the pref is present and use the value. In the end the fragment is the subject of the test. The tearDown cleans the prefs only if modified by the setup... In this way the test doesn't modify pref data if already present.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ashu
Solution 2 Gionata