Android Instrumentation测试失败:OAuth2登录自定义标签页deep link捕获异常
问题背景
编写测试用例验证自定义标签页完成OAuth2登录并通过deep link捕获code的流程,预期测试通过但失败,报错日志显示在ActivityScenario.close()时触发NullPointerException,提示当前状态为null,最后阶段是STARTED。尝试手动调用moveToState(Lifecycle.State.RESUMED)后问题仍存在。
错误日志
java.lang.NullPointerException: Current state was null unexpectedly. Last stage = STARTED at androidx.test.internal.util.Checks.checkNotNull(Checks.java:45) at androidx.test.core.app.ActivityScenario.moveToState(ActivityScenario.java:652) at androidx.test.core.app.ActivityScenario.close(ActivityScenario.java:416) at androidx.test.ext.junit.rules.ActivityScenarioRule.after(ActivityScenarioRule.java:115) at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:59) at androidx.test.rule.GrantPermissionRule$RequestPermissionStatement.evaluate(GrantPermissionRule.java:136) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.ParentRunner.run(ParentRunner.java:413) at org.junit.runners.Suite.runChild(Suite.java:128) at org.junit.runners.Suite.runChild(Suite.java:27) at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.ParentRunner.run(ParentRunner.java:413) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at org.junit.runner.JUnitCore.run(JUnitCore.java:115) at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:67) at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:58) at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:446) at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2361)
问题分析
这个错误的核心是测试结束时,ActivityScenario对应的Activity生命周期状态异常——当通过自定义标签页跳转OAuth登录后,原Activity可能被系统置于后台甚至销毁,导致ActivityScenario内部维护的状态丢失,在测试规则的after阶段执行close()时,无法获取当前状态从而抛出NPE。手动调用moveToState无效,是因为此时Scenario已经无法正常关联到原Activity实例。
解决方案
1. 手动管理ActivityScenario生命周期,替代ActivityScenarioRule
放弃使用ActivityScenarioRule自动管理生命周期,改为手动创建和销毁ActivityScenario,在测试结束前确保Activity处于有效状态:
@Test void testOAuthLoginWithDeepLink() { ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class); // 执行OAuth登录跳转逻辑 scenario.onActivity(activity -> { activity.startOAuthLogin(); }); // 模拟deep link回调,等待code捕获完成 simulateDeepLinkCallback(); // 测试结束前,先将Activity切换到RESUMED状态,再关闭 scenario.moveToState(Lifecycle.State.RESUMED); scenario.close(); }
2. 等待deep link处理完成后再结束测试
使用同步机制(如CountDownLatch)确保deep link的code捕获逻辑完全执行完毕,再进行后续的生命周期操作:
@Test void testOAuthLoginWithDeepLink() throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class); scenario.onActivity(activity -> { // 设置code捕获完成后的回调 activity.setCodeCaptureCallback(() -> { latch.countDown(); }); activity.startOAuthLogin(); }); // 等待回调触发,确保deep link处理完成 latch.await(10, TimeUnit.SECONDS); // 关闭前恢复状态 scenario.moveToState(Lifecycle.State.RESUMED); scenario.close(); }
3. 禁用Activity的自动销毁
在测试中通过ActivityScenario的moveToState保持Activity处于前台状态,避免系统在跳转时销毁它:
@Test void testOAuthLoginWithDeepLink() { ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class); // 保持Activity处于RESUMED状态 scenario.moveToState(Lifecycle.State.RESUMED); // 执行登录跳转和deep link逻辑 // ... // 测试结束正常关闭 scenario.close(); }
4. 检查测试中的Intent处理逻辑
确保deep link的Intent不会导致原Activity被重新创建(比如避免在Manifest中设置launchMode="singleInstance"等可能导致Activity实例重建的配置,或者在测试中处理好Intent的传递)。
内容的提问来源于stack exchange,提问作者microVinchi

