You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用MockitoJUnitRunner实例化AppCompatActivity时抛出空指针异常

问题描述

我想通过MockitoJUnitRunner模拟继承自AppCompatActivity的Activity,虽然知道Android里测试Activity应该用Instrumentation Test,但因为要测试旧代码里写在Activity内的业务逻辑,所以想写单元测试。

我的测试代码如下:

@RunWith(MockitoJUnitRunner.class)
public class SampleActivityTest extends TestCase {

    @Test
    public void testOnCreate() {
        SampleActivity sampleActivity= new SampleActivity();
        assertTrue(true);
    }
}

执行时,SampleActivity sampleActivity= new SampleActivity();这一行抛出NullPointerException,异常堆栈如下:

java.lang.NullPointerException
    at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread(DefaultTaskExecutor.java:77)
    at androidx.arch.core.executor.ArchTaskExecutor.isMainThread(ArchTaskExecutor.java:116)
    at androidx.lifecycle.LifecycleRegistry.enforceMainThreadIfNeeded(LifecycleRegistry.java:316)
    at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:172)
    at androidx.activity.ComponentActivity.<init>(ComponentActivity.java:242)
    at androidx.fragment.app.FragmentActivity.<init>(FragmentActivity.java:103)
    at androidx.appcompat.app.AppCompatActivity.<init>(AppCompatActivity.java:94)
    at com.samsung.android.easysetup.appupdate.view.SampleActivity.<init>(SampleActivity.java:25)
    at com.samsung.android.easysetup.appupdate.view.SampleActivityTest.testOnCreate(SampleActivityTest.java:21)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.mockito.internal.runners.DefaultInternalRunner$1$1.evaluate(DefaultInternalRunner.java:54)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:99)
    at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:105)
    at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:40)
    at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
    at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at com.sun.proxy.$Proxy5.processTestClass(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:119)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
    at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
    at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
    at java.base/java.lang.Thread.run(Thread.java:834)
解决方法

从堆栈可以看出,异常是因为DefaultTaskExecutor.isMainThread()里访问了mMainHandler导致空指针——在单元测试环境中,Android的主线程Handler没有被初始化,而Activity的构造过程会触发Lifecycle相关逻辑,强制检查是否在主线程。

有几种可行的解决方式:

1. 用Mockito模拟Activity,而非直接实例化

既然是测试业务逻辑,不需要真实的Activity实例,直接用Mockito模拟:

@RunWith(MockitoJUnitRunner.class)
public class SampleActivityTest extends TestCase {

    @Mock
    private SampleActivity sampleActivity;

    @Test
    public void testBusinessLogic() {
        // 调用要测试的业务方法,示例:
        when(sampleActivity.calculateSomething()).thenReturn(100);
        assertEquals(100, sampleActivity.calculateSomething());
    }
}

这种方式跳过了真实Activity的构造逻辑,直接模拟对象来测试业务方法。

2. 初始化ArchTaskExecutor的测试环境

如果必须调用真实的业务方法,可以在测试前替换ArchTaskExecutor的实现,让它不检查主线程:

@RunWith(MockitoJUnitRunner.class)
public class SampleActivityTest extends TestCase {

    @Before
    public void setup() {
        // 替换为不检查主线程的TaskExecutor
        ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
            @Override
            public void executeOnDiskIO(Runnable runnable) {
                runnable.run();
            }

            @Override
            public void postToMainThread(Runnable runnable) {
                runnable.run();
            }

            @Override
            public boolean isMainThread() {
                return true; // 直接返回true,绕过检查
            }
        });
    }

    @After
    public void tearDown() {
        // 恢复原来的Executor
        ArchTaskExecutor.getInstance().setDelegate(null);
    }

    @Test
    public void testOnCreate() {
        // 若Context为空,可通过Mockito模拟传入
        SampleActivity sampleActivity = Mockito.spy(new SampleActivity());
        assertTrue(true);
    }
}

3. 拆分业务逻辑到独立类(最优解)

长远来看,把Activity里的业务逻辑抽离到单独的ViewModel、UseCase或者普通Java类中,这样单元测试不需要依赖Android组件,测试起来更简单:

// 抽离的业务类
public class SampleBusinessLogic {
    public int calculateSomething() {
        // 原来Activity里的业务代码
        return 50 + 50;
    }
}

// Activity里调用这个类
public class SampleActivity extends AppCompatActivity {
    private SampleBusinessLogic businessLogic;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        businessLogic = new SampleBusinessLogic();
        int result = businessLogic.calculateSomething();
        // 后续逻辑
    }
}

// 单元测试直接测试业务类
public class SampleBusinessLogicTest {
    @Test
    public void testCalculateSomething() {
        SampleBusinessLogic logic = new SampleBusinessLogic();
        assertEquals(100, logic.calculateSomething());
    }
}

这种方式从根本上解决了依赖Android组件的问题,也是Android开发推荐的架构模式。


内容的提问来源于stack exchange,提问作者Raj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 16:19:55