I took inspiration from google's sample on how to test your SharedPreferences
code here by creating a SharedPreferencesHelper
class:
https://github.com/googlesamples/android-https://github.com/googlesamples/android-testing/blob/master/unit/BasicSample/app/src/main/java/com/example/android/testing/unittesting/BasicSample/SharedPreferencesHelper.java
You can see that the class uses actual strings hardcoded within the class as the keys to the sharedPreferences
- here's an extract of the class:
public class SharedPreferencesHelper {
// Keys for saving values in SharedPreferences.
static final String KEY_NAME = "key_name";
static final String KEY_DOB = "key_dob_millis";
static final String KEY_EMAIL = "key_email";
public boolean savePersonalInfo(SharedPreferenceEntry sharedPreferenceEntry){
// Start a SharedPreferences transaction.
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(KEY_NAME, sharedPreferenceEntry.getName());
editor.putLong(KEY_DOB, sharedPreferenceEntry.getDateOfBirth().getTimeInMillis());
editor.putString(KEY_EMAIL, sharedPreferenceEntry.getEmail());
// Commit changes to SharedPreferences.
return editor.commit();
}
When testing this on using their SharedPreferencesHelperTest
class here, they access the mocked sharedPreferences
using the same variables defined in the above class:
https://github.com/googlesamples/android-testing/blob/master/unit/BasicSample/app/src/test/java/com/example/android/testing/unittesting/BasicSample/SharedPreferencesHelperTest.java
An extract of that class is displayed below:
when(mMockSharedPreferences.getString(eq(SharedPreferencesHelper.KEY_NAME), anyString()))
.thenReturn(mSharedPreferenceEntry.getName());
when(mMockSharedPreferences.getString(eq(SharedPreferencesHelper.KEY_EMAIL), anyString()))
.thenReturn(mSharedPreferenceEntry.getEmail());
when(mMockSharedPreferences.getLong(eq(SharedPreferencesHelper.KEY_DOB), anyLong()))
.thenReturn(mSharedPreferenceEntry.getDateOfBirth().getTimeInMillis());
The way google does this allowed them to bypass the issue of using a context
to pull a string out of the string.xml resource file and query it inside sharedPreferences
like how it is normally suppose to happen by using getString()
, ie:
mSharedPreferences.getString(context.getString(R.string.name),"");
However, I have read that it is not possible to use context.getString
as it is a final method and mockito cannot mock final methods:
Mockito - Overriding a method that takes primitive parameters
How can I then use mockito to unit test any methods with getString
? Any method with getString
will not work and my unit test will fail.
This is my class that I have written for SharedPreferences
and I would like to test it with the sharedPreferences
keys written with getStrings
:
public class SharedPreferencesHelper {
// The injected SharedPreferences implementation to use for persistence.
private final SharedPreferences mSharedPreferences;
private Context context;
public SharedPreferencesHelper(SharedPreferences sharedPreferences, Context context) {
mSharedPreferences = sharedPreferences;
this.context = context;
}
public boolean saveName(String name) {
// Start a SharedPreferences transaction.
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(context.getString(R.string.name), name);
return editor.commit();
}
public String fetchName() {
// Start a SharedPreferences transaction.
return mSharedPreferences.getString(context.getString(R.string.name),"");
}
public boolean saveGender(String gender) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(context.getString(R.string.gender), gender);
return editor.commit();
}
}
This is the test that I have written for the above class - it is very similar to google's SharedPreferencesHelperTest:
@RunWith(MockitoJUnitRunner.class)
public class SharedPreferencesHelperTest {
private static final String TEST_NAME = "Test name";
private SharedPreferencesHelper mMockSharedPreferencesHelper;
@Mock
SharedPreferences mMockSharedPreferences;
@Mock
SharedPreferences.Editor mMockEditor;
@Mock
MockContext context;
@Before
public void setUp() throws Exception {
// Create a mocked SharedPreferences.
mMockSharedPreferencesHelper = createMockSharedPreference();
}
@Test
public void testSaveName() throws Exception {
boolean success = mMockSharedPreferencesHelper.saveName(TEST_NAME);
Timber.e("success " + success);
assertThat("Checking that name was saved... returns true = " + success,
success, is(true));
String name = mMockSharedPreferencesHelper.fetchName();
Timber.e("name " + name);
assertThat("Checking that name has been persisted and read correctly " + name,
TEST_NAME,
is(name));
}
/**
* Creates a mocked SharedPreferences.
*/
private SharedPreferencesHelper createMockSharedPreference() {
// Mocking reading the SharedPreferences as if mMockSharedPreferences was previously written
// correctly.
when(mMockSharedPreferences.getString(Matchers.eq("name"), anyString()))
.thenReturn(TEST_NAME);
when(mMockSharedPreferences.getString(Matchers.eq("gender"), anyString()))
.thenReturn("M");
// Mocking a successful commit.
when(mMockEditor.commit()).thenReturn(true);
// Return the MockEditor when requesting it.
when(mMockSharedPreferences.edit()).thenReturn(mMockEditor);
return new SharedPreferencesHelper(mMockSharedPreferences, context);
}
}
Running the test fails as I have used getString
in my SharedPreferencesHelper
class. If I hard coded the keys, I will not get the error, i.e.:
public String fetchName() {
// Start a SharedPreferences transaction.
return mSharedPreferences.getString("name","");
}
One should not hard-code strings within the code so how do I solve this dilemma?
You could solve this issue by not using the Context
to get the string resource, but the Resources
class. Resources
has the same getString
method but it is not final.
https://developer.android.com/reference/android/content/res/Resources.html#getString(int)
Alternatively you could try out Robolectric, which would solve this issue, because they implement the context.
http://robolectric.org/
2017.06 answer:
I followed jbarat's advice to use Robolectric. It's a bit steep curve to get started because things get changed/deprecated/removed/incompatible with every version of Robolectric/Android Studio.
Here's more or less what I did to make it work, using Android Studio 2.3.3 and Robolectric 3.3.2:
getString
should accept Context
as a first parameter, to make it mockableContext
provided by Robolectricbuild.gradle
:
testCompile "org.robolectric:robolectric:3.3.2"
testCompile "org.robolectric:shadows-multidex:3.3.2"
MyClass.java
:
public static String myGetString(Context context) {
return context.getString(R.string.my_string);
}
Test:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, packageName = "com.myapplication", sdk = 19)
public class MyTest {
private Context mockContext;
@Before
public void setUp() {
// inject context provided by Robolectric
mockContext = RuntimeEnvironment.application.getApplicationContext();
}
@Test
public void test_myGetString() {
assertEquals("MY STRING", MyClass.myGetString(mockContext));
}
}
To enabling mocking of final methods you need Mockito v2 and follow the instructions here Mock the unmockable: opt-in mocking of final classes/methods
create a file in
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
containing a single line:mock-maker-inline
Once you do this you can mock getString() when(mMockContext.getString(R.string.string_name)).thenReturn("Mocked String");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With