如何对方法内部新建的对象进行单元测试?附代码及报错问题
我来帮你拆解下这个问题,你遇到的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内部的潜在问题
- 初始化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(); } }
- 确保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

