15

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?

Community
  • 1
  • 1
Mister Smith
  • 27,417
  • 21
  • 110
  • 193
  • Are your tests under the `test` directory or under `testAndroid`? – Code-Apprentice Feb 05 '16 at 22:45
  • Note that you can still run your old tests which extend `InstrumentationTestCase` or `AndroidTestCase` in Android Studio. – Code-Apprentice Feb 05 '16 at 22:46
  • @Code-Apprentice This test is under the androidTest folder. Yes you can still use `InstrumentationTestCase` and `AndroidTestCase` but Google has sabotaged both and `getContext` returns null. – Mister Smith Feb 05 '16 at 22:49
  • What do you mean "sabotaged"? I have successfully ported all of my tests from an existing project into Android Studio. – Code-Apprentice Feb 05 '16 at 22:54
  • how are you running your tests? Are you running them with android studio unit testing or are you invoking `connectedCheck`? – David Medenjak Feb 05 '16 at 22:54
  • Of course, I primarily (only?) use `ActivityInstrumentationTestCase2`. – Code-Apprentice Feb 05 '16 at 22:55
  • @Code-Apprentice You might have a look at [this question](http://stackoverflow.com/questions/8605611/get-context-of-test-project-in-android-junit-test-case) or do a quick test by yourself. `getContext` will return null. – Mister Smith Feb 05 '16 at 22:56
  • @DavidMedenjak I right click over the test function or class and run it manually. I followed [the official tutorial](http://developer.android.com/intl/es/training/testing/start/index.html#run-instrumented-tests) – Mister Smith Feb 05 '16 at 22:59
  • Are you running it as a JUnit test (the icon with the green and red arrows) or as an Android test (the Android icon)? – Code-Apprentice Feb 05 '16 at 23:02
  • "I can see that the editor is trying to write to this file: `data/data/.test/shared_prefs/PREFS_FILE_NAME.xml` But the file is never created nor written to." How did you verify this? – Code-Apprentice Feb 05 '16 at 23:06
  • @Code-Apprentice I instantiated a new `File` and called `exists()` which returns false. On the other hand, using the main project context produces an actual file and I can even log its content using FileReader. – Mister Smith Feb 05 '16 at 23:09
  • It will help tremendously if you provide relevant code from the class you are testing and example tests that are run against that class to illustrate the different issues you are asking about. – Code-Apprentice Feb 05 '16 at 23:12
  • 1
    @MisterSmith How do you supply your class under test with the SharedPreferences? With what you provide it seems like it is just writing to the default preferences (and getting them itself) – David Medenjak Feb 05 '16 at 23:16
  • When you have your code available, please post the class you are testing and an actual test class. – Code-Apprentice Feb 05 '16 at 23:25
  • @Code-Apprentice Will do. But the class I'm using had passed all the tests in eclipse, and the fact it works with the regular main app context in Android Studio makes me believe the code under test is correct. – Mister Smith Feb 05 '16 at 23:31
  • I'm unclear as to why there is a difference when running the test via Android Studio as opposed to Eclipse. Nothing we are discussing is related to the IDE. It is all about the libraries being used. Are you using a different API version after porting over to Android Studio? – Code-Apprentice Feb 05 '16 at 23:35
  • @Code-Apprentice The two IDE's testing framework are different. Android Studio didn't bring in full support until recently. This could be a "feature" or a bug. I've changed nothing in my classes, which had been already unit tested in old eclipse projects and are in production in 3 clients with no issues. In fact this was a dumb test meant to pass. – Mister Smith Feb 05 '16 at 23:47
  • @MisterSmith When I first ported my Eclipse project to Android Studio 0.5, I was able to run all of the existing tests with the same results. This was over 18 months ago...not very recent in Android-years. Android Studio has had full support of the native libraries in the `android.test` package from the beginning. Recently, the Testing Support Libraries have been released to improve our testing tools. However, you can use these libraries in Eclipse just as easily as in Android Studio. – Code-Apprentice Feb 05 '16 at 23:51
  • With Android Studio are you running the tests on the same device or emulator with the **exact same configuration** as when you ran them in Eclipse? – Code-Apprentice Feb 05 '16 at 23:54
  • @Code-Apprentice Yes, I used 4.2 and 5.0 Samsung devices, and these are among the same we used for previous projects. – Mister Smith Feb 05 '16 at 23:57
  • Have you solved this? I find this question very interesting and would like to help. If you still need help, please post the code for the class which you are testing and a test class. – Code-Apprentice Feb 07 '16 at 20:55

4 Answers4

5

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.

Mister Smith
  • 27,417
  • 21
  • 110
  • 193
4

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.

David Medenjak
  • 33,993
  • 14
  • 106
  • 134
  • I'm pretty sure opening shared preferences using `AndroidTestCase.getContext` resulted in a file being created under the instrumentation apk private files. I've done it for at least 4 eclipse projects and tests passed, and it was a really nice feature since the main apk files weren't modified in the device. Same story with SQLite databases. With Android Studio there's no instrumentation apk file deployed to the device, and that makes me think there's a different mechanism at work here. – Mister Smith Feb 05 '16 at 23:40
  • I must admit I never ran any instrumentation tests with eclipse. I am positive though, that an instrumentation file gets deployed to the phone, how are you checking for it not being there? it usually is listed with its packagename in th elist of installed applications. – David Medenjak Feb 05 '16 at 23:44
  • "With Android Studio there's no instrumentation apk file deployed to the device" Just to clarify, this issue is **not** about Android Studio. It is about the libraries you are using to write your tests. The newer Testing Support Library seems to have some different behaviors. You should still be able to reproduce the exact same behavior as you see in Eclipse using the native instrumentation libraries. – Code-Apprentice Feb 05 '16 at 23:45
  • I was wrong. `AndroidTestCase.getContext` doesn't return the instrumentation context, but the main one. But using `InstrumentationTestCase.getInstrumentation().getContext` doesn't work in eclipse as well, so there's no difference with Android Studio. – Mister Smith Feb 08 '16 at 15:51
  • @DavidMedenjak "will still provide the actual apps preferences, never the ones of your instrumentation" that sentence made me doubt and put me on the right track. The bounty is well awarded. – Mister Smith Feb 16 '16 at 10:07
0

Note that you can still use Android Studio to run your old-style tests which extend InstrumentationTestCase, AndroidTestCase, or ActivityInstrumentationTestCase2 in Android Studio.

The new Testing Support Library provides ActivityTestRule to provide functional testing of a single activity. I believe you need to create one of these objects before attempting to get the Instrumentation and/or Context used for the test. The documentation for Espresso has an example for using this class.

Code-Apprentice
  • 81,660
  • 23
  • 145
  • 268
  • Again, have a look [here](http://stackoverflow.com/questions/8605611/get-context-of-test-project-in-android-junit-test-case). I tell you `AndroidTestCase.getContext` returns null. In any case, I'm not testing activities nor want to create a test activity to run my test. If possible I'd like `AndroidTestCase` to just work as it did in eclipse (I know, we're talking Google here, so maybe this would be asking too much) – Mister Smith Feb 05 '16 at 23:02
  • @MisterSmith Apparently I have misunderstood your question. Can you post some code for the class which you are testing? – Code-Apprentice Feb 05 '16 at 23:07
  • I don't have the code at hand now but the test is very simple: you write to shared preferences and then read to verify the written data is there. – Mister Smith Feb 05 '16 at 23:11
  • Ok I updated the question to show how I open the preferences file in mode private. Other than this, theres nothing pspecial about the code. I just write and commit using `SharedPreferences.Editor` as usual. – Mister Smith Feb 05 '16 at 23:19
0

If you want to test classes without having to create an Activity, I found it's easiest to use Robolectric. Robolectric provides you with a mock context that does everything a context does. In fact, this context is the main reason I use Robolectric for unit testing.

Christine
  • 5,617
  • 4
  • 38
  • 61