为什么AndroidTestCase.getContext()。getApplicationContext()返回null?

时间:2011-06-29 06:15:34

标签: android unit-testing junit android-context

更新2012年2月13日:接受了答案,解释说这种行为是一个错误,并指出它似乎已经比v 1.6更好地消失在模拟器上,这使得它对我们大多数人来说都不是问题。解决方法只是循环/休眠,直到getContext()。getApplicationContext()返回非null。 END UPDATE

根据android.app.Application javadoc,我定义了一个单例(称为数据库),我的所有活动都访问状态和持久数据,而Database.getDatabase(Context)通过Context.getApplicationContext()获取应用程序上下文。当活动将自己传递给getDatabase(Context)时,此设置的工作方式与广告一致,但是当我从AndroidTestCase运行单元测试时,getApplicationContext()调用通常会返回null,尽管测试越长,返回非null的频率越高值。

以下代码在AndroidTestCase中重现null - 演示不需要单例。

首先,要记录app-instantiation消息,在测试中的应用程序中我定义了MyApp并将其添加到清单中。

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

接下来,我定义了一个测试用例,用于报告AndroidTestCase.getContext()4次,由一些睡眠和一个getSharedPreferences()调用分开:

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

这是生成的logCat的一大块(通过Eclipse在SDK11 WinXP上的1.6模拟器):

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)

请注意,getApplicationContext()返回null一段时间,然后开始返回MyApp的实例。我无法在此测试的不同运行中得到完全相同的结果(这就是我在4次迭代,睡眠和调用getSharedPreferences()以试图使应用程序存在的结果。

上面的LogCat消息块似乎最相关,但是单个测试的单个运行的整个LogCat很有趣。 Android启动了4个AndroidRuntimes;上面的块是从第4个。有趣的是,第3个运行时显示消息,指示它在进程ID 447中实例化了MyApp的不同实例:

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)

我假设TestRunner(447)消息来自在过程465中报告其子节点的父测试线程。但问题是:为什么Android在其上下文正确连接到Application实例之前让AndroidTestCase运行?

解决方法:如果我先调用getContext().getSharedPreferences("anyname", Context.MODE_PRIVATE).edit().clear().commit();,我的其中一个测试似乎大部分时间都会避免空值,所以我就是这样。

BTW :如果答案是“这是一个Android错误,你为什么不提交它?哎呀,你为什么不修复它?”然后我愿意做两件事。我还没有采取过成为bug-filer或贡献者的步骤 - 也许现在是个好时机。

2 个答案:

答案 0 :(得分:12)

Instrumentation在与主应用程序线程的单独线程中运行,因此它可以在不阻塞或中断(或被主线程阻塞)的情况下执行。如果需要与主线程同步,请使用例如:Instrumentation.waitForIdleSync()

特别是,Application对象以及Activity之类的所有其他顶级类都由主线程初始化。您的检测线程正在初始化时运行。如果您触摸任何这些对象并且没有实现自己的线程安全措施,则应该在主线程上运行此类代码,例如:Instrumentation.runOnMainSync(java.lang.Runnable)

答案 1 :(得分:4)

正如问题和Dianne的答案(@hackbod)中所提到的,Instrumentation在一个单独的线程上运行。 AndroidTestCase要么有实现缺陷(缺少同步),要么没有正确记录。遗憾的是,无法从此特定测试用例类中调用Instrumentation.waitForIdleSync(),因为无法从中访问Instrumentation。

此子类可用于添加轮询getApplicationContext()的同步,直到它返回非null值:

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

轮询和睡眠持续时间基于经验,如有必要可以调整。