Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mockito: How do I use getString with mockito?

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?

like image 967
Simon Avatar asked Jul 10 '16 20:07

Simon


3 Answers

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/

like image 135
jbarat Avatar answered Oct 17 '22 03:10

jbarat


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:

  • All the methods in your code that are using getString should accept Context as a first parameter, to make it mockable
  • Test needs to be annotated properly, so that they run with Robolectric, and not default JUnit runner
  • Test needs to inject mocked Context provided by Robolectric
  • Android Studio needs to be configured, see
    • http://robolectric.org/getting-started/#building-with-android-studio
    • https://github.com/robolectric/robolectric.github.io/pull/41/files

build.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));
    }
}
like image 33
jakub.g Avatar answered Oct 17 '22 03:10

jakub.g


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");

like image 1
Adeel Ahmad Avatar answered Oct 17 '22 04:10

Adeel Ahmad