Почему AndroidTestCase.getContext().getApplicationContext() возвращает значение null?

ОБНОВЛЕНИЕ 13 февраля 2012 г .: Принят ответ, объяснено, что такое поведение является ошибкой, и отмечено, что оно исчезло в эмуляторах лучше, чем v 1.6, что делает его не проблемой для большинства из нас. Обходной путь - просто зацикливаться/спать, пока getContext().getApplicationContext() не вернет ненулевое значение. ЗАВЕРШИТЬ ОБНОВЛЕНИЕ

В соответствии с android.app.Application javadoc я определил синглтон (называемый базой данных), к которому все мои действия обращаются для состояния и постоянных данных, а Database.getDatabase(Context) получает контекст приложения через Context.getApplicationContext(). Эта настройка работает, как рекламируется, когда действия передаются в getDatabase(Context), но когда я запускаю модульный тест из AndroidTestCase, вызов getApplicationContext() часто возвращает значение null, хотя чем дольше тест, тем чаще он возвращает ненулевое значение. ценность.

Следующий код воспроизводит нуль в AndroidTestCase — синглтон не нужен для демонстрации.

Во-первых, для регистрации сообщений об экземплярах приложений в тестируемом приложении я определил 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 (эмулятор 1.6 на SDK11 WinXP через 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)

Обратите внимание, что getApplicationContext() какое-то время возвращал значение null, а затем начал возвращать экземпляр MyApp. Мне не удалось получить одинаковые результаты в разных прогонах этого теста (вот как я закончил 4 итерации, сон и этот вызов getSharedPreferences(), чтобы попытаться создать приложение).

Фрагмент сообщений LogCat выше показался наиболее важным, но весь LogCat для этого единственного запуска этого единственного теста был интересен. Android запустил 4 среды выполнения Android; кусок выше был из 4-го. Интересно, что третья среда выполнения отображала сообщения, указывающие на то, что она создала другой экземпляр MyApp в процессе с идентификатором 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)

Я предполагаю, что сообщения TestRunner(447) исходят от родительского тестового потока, сообщающего о своих дочерних элементах в процессе 465. Тем не менее, возникает вопрос: почему Android позволяет запустить AndroidTestCase до того, как его контекст будет должным образом подключен к экземпляру приложения?

Временное решение. В одном из моих тестов, похоже, в большинстве случаев не было пустых значений, если я сначала вызывал getContext().getSharedPreferences("anyname", Context.MODE_PRIVATE).edit().clear().commit();, поэтому я использую его.

Кстати: если ответ "это ошибка Android, почему бы вам не зарегистрировать ее; черт возьми, почему бы вам ее не исправить?" тогда я был бы готов сделать и то, и другое. Я еще не решился стать баг-файлером или участником — может быть, сейчас самое подходящее время.


person cdhabecker    schedule 29.06.2011    source источник
comment
Я также заметил странное поведение с AndroidTestCase. Обычно каждый тест запускается несколько раз, но только один раз отправляет отчет в Eclipse. Например, я иногда получаю Nullpointer исключений в onDestroy, тогда как все поля экземпляра были правильно инициализированы в onCreate. Вы можете проверить это, поставив точку останова в onDestroy и увидеть, что выполнение прерывается там как минимум 4 раза, в то время как тест запускался только один раз.   -  person siamii    schedule 19.07.2011
comment
У меня была такая же проблема на эмуляторе Android 4.0.4, где ApplicationContext иногда присутствовал, а иногда нет. Ваш обходной путь сна, пока он не появится, кажется, работает.   -  person Ralf    schedule 31.10.2012
comment
У меня это также проявляется в тестах, запущенных на эмуляторе HAX с SDK 10. В противном случае похоже, что ошибка больше не может быть воспроизведена.   -  person Snicolas    schedule 10.06.2013


Ответы (2)


Инструментарий запускается в потоке, отдельном от основного потока приложения, поэтому он может выполняться, не блокируя и не нарушая (или блокируя) основной поток. Если вам нужно синхронизироваться с основным потоком, используйте, например: Instrumentation.waitForIdleSync()

В частности, объект Application, а также все другие классы верхнего уровня, такие как Activity, инициализируются основным потоком. Ваш инструментальный поток работает в то же время, когда они инициализируются. Это, если вы касаетесь любого из этих объектов и не реализуете свои собственные меры безопасности потоков, вам, вероятно, следует запускать такой код в основном потоке, например через: Instrumentation.runOnMainSync(java.lang.Runnable)

person hackbod    schedule 29.11.2011
comment
Я думаю, что вы правильно определили причину поведения, поэтому я принимаю этот ответ. Поведение по-прежнему является дефектом; предполагается, что классы фреймворка завершат настройку до запуска методов тестового примера. А именно, AndroidTestCase.getContext() javadoc не содержит предварительных требований; на самом деле, getContext() возвращает значение, а getContext().getApplicationContext() этого не делает (какое-то время). Идея синхронизации инструментария хороша, но не применима к AndroidTestCase. Спасибо, в любом случае. НАКОНЕЦ -- я могу воспроизвести это на эмуляторе 1.6, но не на 2.1, 2.3.3 или 3.0. Итак, давайте назовем это день! :-) - person cdhabecker; 14.02.2012

Как упоминалось в вопросе и ответе Дайанны (@hackbod), инструментарий работает в отдельном потоке. AndroidTestCase либо имеет дефект реализации (отсутствует синхронизация), либо неправильно задокументирован. К сожалению, нет возможности вызвать Instrumentation.waitForIdleSync() из этого конкретного класса тестового примера, потому что инструментарий из него недоступен.

Этот подкласс можно использовать для добавления синхронизации, которая опрашивает getApplicationContext() до тех пор, пока не вернет ненулевое значение:

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

Продолжительность опроса и сна зависит от опыта и при необходимости может быть настроена.

person James Wald    schedule 27.11.2013
comment
Эта проблема затрагивает устройства ниже API 16 Jelly Bean. - person James Wald; 05.01.2014