studio - ¿Por qué AndroidTestCase.getContext(). GetApplicationContext() devuelve nulo?
instrumentationregistry not found (2)
ACTUALIZACIÓN 13/1/2012: aceptó una respuesta, explicó que este comportamiento es un error, y señaló que parece haber desaparecido en los emuladores mejor que v 1.6, lo que lo convierte en un problema para la mayoría de nosotros. La solución alternativa es simplemente repetir / dormir hasta que getContext (). GetApplicationContext () devuelva un valor no nulo. FINALIZAR ACTUALIZACIÓN
De acuerdo con android.app.application javadoc, definí un singleton (llamado Database) al que todas mis actividades acceden para datos de estado y persistentes, y Database.getDatabase (Context) obtiene el contexto de la aplicación a través de Context.getApplicationContext (). Esta configuración funciona como se anuncia cuando las actividades se transfieren a getDatabase (Contexto), pero cuando ejecuto una prueba unitaria desde una AndroidTestCase, la llamada a getApplicationContext () a menudo devuelve nulo, aunque cuanto más larga es la prueba, más frecuentemente devuelve un valor nulo. valor.
El siguiente código reproduce el nulo dentro de una AndroidTestCase: el singleton no es necesario para la demostración.
Primero, para registrar mensajes de instanciación de aplicaciones, en la aplicación bajo prueba definí MyApp y lo agregué al manifiesto.
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Log.i("MYAPP", "this=" + this);
Log.i("MYAPP", "getAppCtx()=" + getApplicationContext());
}
}
A continuación, definí un caso de prueba para informar sobre AndroidTestCase.getContext () 4 veces, separado por algunos sleeps y una llamada 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.");
}
}
}
Y esto es una parte del logCat resultante (emulador de 1.6 en SDK11 WinXP a través de 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)
Observe que getApplicationContext () devolvió el valor nulo por un tiempo y luego comenzó a devolver una instancia de MyApp. No he podido obtener exactamente los mismos resultados en diferentes versiones de esta prueba (así es como terminé en 4 iteraciones, duerme y esa llamada a getSharedPreferences () para intentar poner en marcha la aplicación).
El fragmento de mensajes de LogCat anterior parecía más relevante, pero todo el LogCat para esa única ejecución de esa única prueba fue interesante. Android comenzó 4 AndroidRuntimes; el pedazo de arriba era del 4to. Curiosamente, el tercer tiempo de ejecución mostraba mensajes que indicaban que creaba una instancia diferente de MyApp en el proceso 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)
Supongo que los mensajes de TestRunner (447) provienen de un hilo de prueba padre que informa sobre sus hijos en el proceso 465. Aún así, la pregunta es: ¿por qué Android permite que se ejecute AndroidTestCase antes de que su contexto se conecte correctamente a una instancia de aplicación?
Solución alternativa : una de mis pruebas parecía evitar nulos la mayor parte del tiempo si llamaba a getContext().getSharedPreferences("anyname", Context.MODE_PRIVATE).edit().clear().commit();
primero, así que voy con eso.
Por cierto : si la respuesta es "es un error de Android, ¿por qué no lo archivas, demonios, por qué no lo arreglas?" entonces estaría dispuesto a hacer ambas cosas. Todavía no he dado el paso de ser un archivador de errores o colaborador, tal vez este sea un buen momento.
Como se menciona en la pregunta y en la respuesta de Dianne (@hackbod), la instrumentación se ejecuta en un hilo separado. AndroidTestCase
tiene un defecto de implementación (falta de sincronización) o no está documentado correctamente. Desafortunadamente, no hay forma de llamar a Instrumentation.waitForIdleSync()
desde esta clase particular de casos de prueba porque no se puede acceder a la Instrumentación desde allí.
Esta subclase se puede usar para agregar sincronización que sondea getApplicationContext () hasta que devuelva un valor no nulo:
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);
}
}
}
Las duraciones de sondeo y de suspensión se basan en la experiencia y pueden ajustarse si es necesario.
La instrumentación se ejecuta en un subproceso separado del subproceso de la aplicación principal, por lo que se puede ejecutar sin bloquear o interrumpir (o ser bloqueado por) el hilo principal. Si necesita sincronizar con el hilo principal, use por ejemplo: Instrumentation.waitForIdleSync()
En particular, el objeto Aplicación, así como todas las demás clases de nivel superior, como Actividad, se inicializan mediante el hilo principal. Su hilo de instrumentación se ejecuta al mismo tiempo que se están inicializando. Si toca alguno de esos objetos y no implementa sus propias medidas de seguridad de subprocesos, probablemente debería ejecutar dicho código en el subproceso principal, como por ejemplo: Instrumentation.runOnMainSync(java.lang.Runnable)