Подтвердить что ты не робот

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

ОБНОВЛЕНИЕ 2/13/2012: Принял ответ, объяснил, что это поведение является ошибкой, и отметил, что он, по-видимому, исчез на эмуляторах лучше, чем v 1.6, что делает его не проблемой для большинства из нас. Обходным путем является просто цикл/спящий режим до получения getContext(). GetApplicationContext() возвращает ненулевое значение. END UPDATE

Как и в android.app.Application javadoc, я определил singleton (называемый Database), что весь доступ к моим действиям для состояния и постоянных данных, а Database.getDatabase(Context) получает контекст приложения через Context.getApplicationContext(). Эта настройка работает так, как рекламируется, когда действия передаются в getDatabase (Context), но когда я запускаю unit test из 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): [email protected] pkg=com.foo.android
INFO/XPLORE1(465): testContext.getAppCtx()=null
INFO/XPLORE1(465): [email protected] pkg=com.foo.android
INFO/XPLORE1(465): appContext.getAppCtx()=null
INFO/XPLORE2(465): [email protected] pkg=com.foo.android
INFO/XPLORE2(465): testContext.getAppCtx()=null
INFO/XPLORE2(465): [email protected] pkg=com.foo.android
INFO/XPLORE2(465): appContext.getAppCtx()=null
INFO/MYAPP(465): [email protected]
INFO/MYAPP(465): getAppCtx()[email protected]
INFO/XPLORE3(465): [email protected] pkg=com.foo.android
INFO/XPLORE3(465): testContext.getAppCtx()[email protected]
INFO/XPLORE3(465): [email protected] pkg=com.foo.android
INFO/XPLORE3(465): appContext.getAppCtx()[email protected]
INFO/XPLORE4(465): [email protected] pkg=com.foo.android
INFO/XPLORE4(465): testContext.getAppCtx()[email protected]
INFO/XPLORE4(465): [email protected] pkg=com.foo.android
INFO/XPLORE4(465): appContext.getAppCtx()[email protected]
INFO/TestRunner(465): finished: test_exploreContext(test.foo.android.DatabaseTest)

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

Час сообщений LogCat выше выглядел наиболее актуальным, но весь LogCat для этого одиночного запуска этого единственного теста был интересен. Android начал 4 AndroidRuntimes; кусок выше был с 4-го. Интересно, что 3-я среда выполнения отображала сообщения, указывающие, что она создала экземпляр MyApp другого экземпляра в ID 447 процесса:

INFO/TestRunner(447): started: test_exploreContext(test.foo.android.DatabaseTest)
INFO/MYAPP(447): [email protected]
INFO/MYAPP(447): getAppCtx()[email protected]
INFO/TestRunner(447): finished: test_exploreContext(test.foo.android.DatabaseTest)

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

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

Как упоминалось в вопросе и ответе Дианны (@hackbod), Instrumentation работает в отдельном потоке. 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);
        }
    }
}

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