Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Studio: Cannot write to Shared Preferences in instrumented test

I'm trying to write a test case to verify a class that writes to Shared Preferences. I'm using Android Studio v1.5.

In the good old eclipse, when using AndroidTestCase, a second apk file was deployed to the device, and tests could be run using the instrumentation context, so you could run tests using the instrumentation apk's shared preferences without altering the main apk's existing shared preferences files.

I've spent the entire morning trying to figure out how to get a non null context in Android Studio tests. Apparently unit tests made for eclipse are not compatible with the Android Studio testing framework, as calling getContext() returns null. I thought I've found the answer in this question: Get context of test project in Android junit test case

Things have changed over time as old versions of Android Studio didn't have full testing support. So a lot of answers are just hacks. Apparently now instead of extending InstrumentationTestCase or AndroidTestCase you should write your tests like this:

@RunWith(AndroidJUnit4.class)
public class MyTest {

    @Test
    public void testFoo(){
        Context instrumentationContext = InstrumentationRegistry.getContext();
        Context mainProjectContext = InstrumentationRegistry.getTargetContext();            
    }   
}

So I now have a non null instrumentation context, and the getSharedPreferences method returns an instance that seems to work, but actually no preferences file is being written.

If I do:

context = InstrumentationRegistry.getContext();      

Then the SharedPreferences editor writes and commits correctly and no exception is thrown. On closer inspection I can see that the editor is trying to write to this file:

data/data/<package>.test/shared_prefs/PREFS_FILE_NAME.xml

But the file is never created nor written to.

However using this:

context = InstrumentationRegistry.getTargetContext(); 

the editor works correctly and the preferences are written to this file:

/data/data/<package>/shared_prefs/PREFS_FILE_NAME.xml

The preferences are instantiated in private mode:

SharedPreferences sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);

As far as I know, no test apk has been uploaded to the device after running the test. This might explain why the file was not written using the instrumentation context. Is it possible that this context is a fake context that fails silently?

And if this were the case, how could I obtain a REAL instrumentation context so that I can write preferences without altering the main project's preferences?

like image 458
Mister Smith Avatar asked Feb 03 '16 15:02

Mister Smith


People also ask

What is shared preference in Android Studio?

Shared Preference Tutorial With Example In Android Studio. Shared Preference in Android are used to save data based on key-value pair. If we go deep into understanding of word: shared means to distribute data within and preference means something important or preferable, so SharedPreferences data is shared and preferred data.

Is it possible to write unit tests for sharedpreferences?

I don't have experience writing unit tests for SharedPreferences. I would generally expect that you instead use instrumented tests with a real prefs store (that you set up and tear down as appropriate), but even there I don't have real experience actually doing it. Show activity on this post.

What is shared preferences mock in Android?

TL;DR: Shared preferences mock is the lightweight library let you increase coverage of unit tests and simplify code for them with one line of code. Unit test on Android uses a framework mock where every method throws UnsupportedOperationException or does nothing depending on your settings in Gradle testOptions.

How to implement an Android framework using instrumented unit tests?

Using instrumented Unit Tests, we can have real implementation of an Android Framework component like SharedPreferences objects, context of an Activity etc. Also , we will not have to mock any objects.1. Add the following Dependencies : note that we are using androidTestCompile as instrumented tests will be inside androidTest folder. 2.


2 Answers

Turns out you can't write to shared preferences using the instrumentation context, and this was true even in eclipse. This would be the equivalent test for eclipse:

import android.content.Context;
import android.content.SharedPreferences;
import android.test.InstrumentationTestCase;

public class SharedPrefsTest extends InstrumentationTestCase {

    public void test() throws Exception { 
        Context context = getInstrumentation().getContext();
        String fileName = "FILE_NAME";

        SharedPreferences sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString("key", "value");
        editor.commit();

        SharedPreferences sharedPreferences2 = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
        assertEquals("value", sharedPreferences2.getString("key", null));
    }
}

I just ran it and it also fails. The preferences are never written. I think internal storage file access is forbidden in this context, as calling Context.getFilesDir() throws an InvocationTargetException, and so does calling File.exists() over the preferences file (you can check which file is the editor writing to using the debugger, just look for a private variable called mFile inside the this.$0 member instance).

So I was wrong in thinking this was actually possible. I though we had used the instrumentation context for data access layer testing in the past, but we actually used the main context (AndroidTestCase.getContext()), although we used different names for the preferences and SQLite files. And this is why the unit tests didn't modify the regular app files.

like image 89
Mister Smith Avatar answered Oct 19 '22 18:10

Mister Smith


The instrumentation will be installed alongside with your application. The application will run itself, thus reading and writing its own SharedPreferences.

It is odd, that the SharedPreferences of the Instrumentation get deleted (or never created), but even if they would be created, you would have a hard time passing them into your application under test. As stated above just calling context.getSharedPreferences(); inside your app will still provide the actual apps preferences, never the ones of your instrumentation.

You will need to find a way to provide the preferences to your application under test. A good solution to this would be to keep the preferences in your Application like the following:

public class App extends Application {
    SharedPreferences mPreferences;
    public void onCreate() {
        mPreferences = getSharedPreferences(fileName, Context.MODE_PRIVATE);
    }

    // add public getter / setter
}

This way, you can

  1. As long as you have a context get the preferences from a single source using ((App) context.getApplicationContext()).getPreferences()
  2. Set the preferences yourself before running your tests and starting any activities to inject your test data.

In your test setup then call the following to inject any preferences you need

@Before
public void before() {
    ((App) InstrumentationRegistry.getTargetContext()).setPreferences(testPreferences);
}

Be sure to correctly finish off activities after each test, so that each test can get their own dependencies.

Also, you should strongly think about just mocking the SharedPreferences using Mockito, other frameworks, or simply implementing the SharedPreferences interface yourself, since this greatly simplifies verifying interactions with models.

like image 3
David Medenjak Avatar answered Oct 19 '22 17:10

David Medenjak