'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.
- 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 theSharedPreferenceswhen tearing down the test (in the@Aftermethod). - Inject
SharedPreferencesinFragmentafter it has been created, by passing a callback toFragmentScenario#onFragment(). You can inject a mockedSharedPreferencesthis 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. - 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 |
