Why does AndroidTestCase.getContext().getApplicationContext() return null?

10,906

Solution 1

Instrumentation runs in a separate thread from the main app thread, so that it can execute without blocking or disrupting (or being blocked by) the main thread. If you need to synchronize with the main thread, use for example: Instrumentation.waitForIdleSync()

In particular, the Application object as well as all other top-level classes like Activity are initialized by the main thread. Your instrumentation thread is running at the same time those are initializing. This if you are touching any of those objects and are not implementing your own measures of thread safety, you should probably have such code run on the main thread such as via: Instrumentation.runOnMainSync(java.lang.Runnable)

Solution 2

As mentioned in the question and Dianne's answer (@hackbod), the Instrumentation runs on a separate thread. AndroidTestCase either has an implementation defect (missing synchronization) or it is not documented correctly. Unfortunately, there is no way to call Instrumentation.waitForIdleSync() from this particular test case class because the Instrumentation is not accessible from it.

This subclass can be used to add synchronization that polls getApplicationContext() until it returns a non-null value:

public class MyAndroidTestCase extends AndroidTestCase {

    @Override
    public void setContext(Context context) {
        super.setContext(context);

        long endTime = SystemClock.elapsedRealtime() + TimeUnit.SECONDS.toMillis(2);

        while (null == context.getApplicationContext()) {

            if (SystemClock.elapsedRealtime() >= endTime) {
                fail();
            }

            SystemClock.sleep(16);
        }
    }
}

The polling and sleep durations are based on experience and can be tuned if necessary.

Share:
10,906
cdhabecker
Author by

cdhabecker

Android Apps Engineer at NOOK Media LLC.

Updated on June 05, 2022

Comments

  • cdhabecker
    cdhabecker about 2 years

    UPDATE 2/13/2012: Accepted an answer, explained that this behavior is a bug, and noted that it appears to have disappeared on emulators better than v 1.6, which makes it a non-issue for most of us. The workaround is simply to loop/sleep until getContext().getApplicationContext() returns non-null. END UPDATE

    As per android.app.Application javadoc, I defined a singleton (called Database) that all of my activities access for state and persistent data, and Database.getDatabase(Context) gets the application context via Context.getApplicationContext(). This setup works as advertised when activities pass themselves to getDatabase(Context), but when I run a unit test from an AndroidTestCase, the getApplicationContext() call often returns null, though the longer the test, the more frequently it returns a non-null value.

    The following code reproduces the null within an AndroidTestCase -- the singleton isn't necessary for the demonstration.

    First, to log app-instantiation messages, in the app-under-test I defined MyApp and added it to the manifest.

    public class MyApplication extends Application {
        @Override
        public void onCreate() {
            super.onCreate();
            Log.i("MYAPP", "this=" + this);
            Log.i("MYAPP", "getAppCtx()=" + getApplicationContext());
        }
    }
    

    Next, I defined a test case to report on AndroidTestCase.getContext() 4 times, separated by some sleeps and a getSharedPreferences() call:

    public class DatabaseTest extends AndroidTestCase {
        public void test_exploreContext() {
            exploreContexts("XPLORE1");
            getContext().getSharedPreferences("foo", Context.MODE_PRIVATE);
            exploreContexts("XPLORE2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            exploreContexts("XPLORE3");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            exploreContexts("XPLORE4");
        }
        public void exploreContexts(String tag) {
            Context testContext = getContext();
            Log.i(tag, "testCtx=" + testContext + 
                    " pkg=" + testContext.getApplicationInfo().packageName);
            Log.i(tag, "testContext.getAppCtx()=" + testContext.getApplicationContext());
            try {
                Context appContext = testContext.createPackageContext("com.foo.android", 0);
                ApplicationInfo appInfo = appContext.getApplicationInfo();
                Log.i(tag, "appContext=" + appContext +
                        " pkg=" + appContext.getApplicationInfo().packageName);
                Log.i(tag, "appContext.getAppCtx()=" + appContext.getApplicationContext());
            } catch (NameNotFoundException e) {
                Log.i(tag, "Can't get app context.");
            }
        }
    }
    

    And this is a chunk of the resulting logCat (1.6 emulator on SDK11 WinXP via Eclipse):

    INFO/TestRunner(465): started: test_exploreContext(test.foo.android.DatabaseTest)
    INFO/XPLORE1(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
    INFO/XPLORE1(465): testContext.getAppCtx()=null
    INFO/XPLORE1(465): appContext=android.app.ApplicationContext@437801e8 pkg=com.foo.android
    INFO/XPLORE1(465): appContext.getAppCtx()=null
    INFO/XPLORE2(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
    INFO/XPLORE2(465): testContext.getAppCtx()=null
    INFO/XPLORE2(465): appContext=android.app.ApplicationContext@43782820 pkg=com.foo.android
    INFO/XPLORE2(465): appContext.getAppCtx()=null
    INFO/MYAPP(465): this=com.foo.android.MyApplication@43783830
    INFO/MYAPP(465): getAppCtx()=com.foo.android.MyApplication@43783830
    INFO/XPLORE3(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
    INFO/XPLORE3(465): testContext.getAppCtx()=com.foo.android.MyApplication@43783830
    INFO/XPLORE3(465): appContext=android.app.ApplicationContext@43784768 pkg=com.foo.android
    INFO/XPLORE3(465): appContext.getAppCtx()=com.foo.android.MyApplication@43783830
    INFO/XPLORE4(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
    INFO/XPLORE4(465): testContext.getAppCtx()=com.foo.android.MyApplication@43783830
    INFO/XPLORE4(465): appContext=android.app.ApplicationContext@43785778 pkg=com.foo.android
    INFO/XPLORE4(465): appContext.getAppCtx()=com.foo.android.MyApplication@43783830
    INFO/TestRunner(465): finished: test_exploreContext(test.foo.android.DatabaseTest)
    

    Notice that getApplicationContext() returned null for a while, then started returning an instance of MyApp. I have not been able to get the exact same results in different runs of this test (that's how I ended up at 4 iterations, sleeps, and that call to getSharedPreferences() to try to goose the app into existence).

    The chunk of LogCat messages above seemed most relevant, but the entire LogCat for that single run of that single test was interesting. Android started 4 AndroidRuntimes; the chunk above was from the 4th. Interestingly, the 3rd runtime displayed messages indicating that it instantiated a different instance of MyApp in process ID 447:

    INFO/TestRunner(447): started: test_exploreContext(test.foo.android.DatabaseTest)
    INFO/MYAPP(447): this=com.foo.android.MyApplication@437809b0
    INFO/MYAPP(447): getAppCtx()=com.foo.android.MyApplication@437809b0
    INFO/TestRunner(447): finished: test_exploreContext(test.foo.android.DatabaseTest)
    

    I assume that the TestRunner(447) messages are from a parent test thread reporting on its children in process 465. Still, the question is: why does Android let an AndroidTestCase run before its context is properly hooked up to an Application instance?

    Workaround: One of my tests seemed to avoid nulls most of the time if I called getContext().getSharedPreferences("anyname", Context.MODE_PRIVATE).edit().clear().commit(); first, so I'm going with that.

    BTW: If the answer is "it's an Android bug, why don't you file it; heck, why don't you fix it?" then I'd be willing to do both. I haven't taken the step of being a bug-filer or contributor yet -- maybe this is a good time.

  • cdhabecker
    cdhabecker over 12 years
    I think that you correctly identified the cause of the behavior, so I'm accepting this answer. The behavior is still a defect; the framework classes are supposed to complete setup before test case methods run. To wit, AndroidTestCase.getContext() javadoc lists no pre-reqs; in fact, getContext() returns a value, it is getContext().getApplicationContext() that doesn't (for a while). The Instrumentation sync idea is nice, but not applicable to AndroidTestCase. Thanks anyway. FINALLY -- I can reproduce this on 1.6 emulator, but not on 2.1, 2.3.3, or 3.0. So let's call it a day! :-)
  • James Wald
    James Wald over 10 years
    This issue appears to affect devices below API 16 Jelly Bean.