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

如何对方法内部新建的对象进行单元测试?附代码及报错问题

解决PaymentPresenter单元测试的NullPointerException及内部对象测试问题

我来帮你拆解下这个问题,你遇到的NullPointerException和方法内部新建对象的测试难点其实是关联在一起的,咱们一步步解决:

一、先搞懂NullPointerException的根源

你的测试代码里自己new了一个PaymentRequest,然后用它来StubworkOrdersRepository.payWorkOrderInvoice()的返回值,但Presenter方法内部是重新new了一个完全独立的PaymentRequest实例。如果PaymentRequest和WoPayment没有重写equals()和hashCode()方法,Mockito会认为这两个对象是不相等的,导致你的Stub完全不生效——调用payWorkOrderInvoice()时返回的是null,后续调用subscribe()自然就抛出NPE了。

另外,Presenter里的disposables初始化逻辑也有隐患,如果初始为null,RxUtil.initDisposables()可能返回null,也会触发NPE。

二、正确测试内部新建对象的两种方案

方案1:用ArgumentCaptor捕获内部对象(推荐)

这个方法不需要依赖对象的equals()方法,还能精准验证Presenter内部构建的对象属性是否符合预期,是测试内部新建对象的标准做法:

修改你的测试代码:

@Test
public void shouldPayWorkOrderInvoice() {
    // Given
    int workOrderId = 1;
    double amount = 1.0;
    String paymentMethod = "cash";
    String checkNumber = "1";
    WorkOrderDetails workOrderDetails = Mockito.mock(WorkOrderDetails.class);
    Response<WorkOrderDetails> response = Response.success(200, workOrderDetails);

    // 定义ArgumentCaptor,用来捕获Presenter内部创建的PaymentRequest
    ArgumentCaptor<PaymentRequest> paymentRequestCaptor = ArgumentCaptor.forClass(PaymentRequest.class);

    // When - 先用any()匹配PaymentRequest,确保Stub能命中
    Mockito.when(workOrdersRepository.payWorkOrderInvoice(eq(workOrderId), any(PaymentRequest.class)))
           .thenReturn(Single.just(response));

    // 调用Presenter的目标方法
    presenter.payWorkOrderInvoice(workOrderId, amount, paymentMethod, checkNumber);

    // Then
    // 第一步:捕获实际传入Repository的PaymentRequest
    Mockito.verify(workOrdersRepository)
           .payWorkOrderInvoice(eq(workOrderId), paymentRequestCaptor.capture());
    
    // 第二步:验证内部构建的WoPayment属性是否正确(假设PaymentRequest有getWoPayment()方法)
    PaymentRequest capturedRequest = paymentRequestCaptor.getValue();
    WoPayment capturedWoPayment = capturedRequest.getWoPayment();
    assertEquals(amount, capturedWoPayment.getAmount(), 0.001);
    assertEquals(paymentMethod, capturedWoPayment.getPaymentMethod());
    assertEquals(checkNumber, capturedWoPayment.getCheckNumber());

    // 第三步:验证View的交互是否符合预期
    Mockito.verify(view).disablePayButton();
    Mockito.verify(view).displayWorkOrderInvoicePaid(workOrderDetails);
    Mockito.verify(view, never()).enablePayButton();
}

方案2:重写PaymentRequest和WoPayment的equals/hashCode

如果你的业务场景允许,可以给这两个类重写equals()和hashCode()方法,基于对象的属性值进行比较。这样Mockito就能匹配到你测试里手动创建的PaymentRequest,Stub会生效。不过这个方案的灵活性不如ArgumentCaptor,只能验证对象相等,没法检查构建过程中的细节。

三、修复Presenter内部的潜在问题

  1. 初始化disposables:把disposables的初始化移到Presenter的构造方法里,避免null风险:
public class PaymentPresenter {
    private CompositeDisposable disposables = new CompositeDisposable();

    // 构造方法和其他代码...

    public void payWorkOrderInvoice(int workOrderId, double amount, String paymentMethod, String checkNumber) {
        // 去掉原来的disposables = RxUtil.initDisposables(disposables);
        // 改用clear确保之前的订阅被清理
        if (!disposables.isDisposed()) {
            disposables.clear();
        }

        // 后续原有逻辑...
    }

    // 记得在Presenter销毁时清理订阅
    public void onDestroy() {
        disposables.dispose();
    }
}
  1. 确保TextUtils的Mock生效:检查你的mockTextUtils()方法,确保正确mock了TextUtils.isEmpty()的行为:
private void mockTextUtils() {
    PowerMockito.mockStatic(TextUtils.class);
    PowerMockito.when(TextUtils.isEmpty(anyString())).thenAnswer(invocation -> {
        String str = invocation.getArgument(0);
        return str == null || str.trim().isEmpty();
    });
}

四、确保RxJava测试的线程同步

你的TrampolineSchedulerRule需要确保替换所有RxJava的调度器,这样测试时订阅会同步执行,避免verify时还没收到回调:

public class TrampolineSchedulerRule implements TestRule {
    @Override
    public Statement apply(final Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                // 替换所有RxJava调度器为trampoline(同步执行)
                RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
                RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
                RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
                // 替换Android主线程调度器
                RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());

                try {
                    base.evaluate();
                } finally {
                    // 测试结束后重置调度器,避免影响其他测试
                    RxJavaPlugins.reset();
                    RxAndroidPlugins.reset();
                }
            }
        };
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:59:21