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

如何用Mockito和Lombok.Builder测试Optional的orElseGet()并解决NPE

问题描述

我有一个实现SamplePredicateFactory接口的类IsSampleTrue,里面包含一个谓词函数。当SampleContext中的MainObject不存在时,会通过Optional的orElseGet()方法手动构建MainObject。但执行testMainObjAbsentFromContext单元测试时,调用buildSamplePredicate方法抛出NullPointerException,而testMainObjPresent测试能正常通过。我不太熟悉Mockito的用法,求解决指引。


相关代码

包含谓词函数的类IsSampleTrue

@RequiredArgsConstructor
public class IsSampleTrue implements SamplePredicateFactory {
    private final SampleValueCalculator sampleValCalc;
    private final SampleParameterManager<SampleParameter> sampleParameterMgr;
    private final Cache<String, SampleData> sampleCache;

    @Override
    public SamplePredicate buildSamplePredicate(SampleContext ctx) {

       // 若上下文没有预创建的MainObj,手动构建
       MainObject mainObj = ctx.getMainObject.orElseGet(() -> {
           return MainObject.builder()
               .a(ctx.getA())
               .b(ctx.getB())
               .c(ctx.getC())
               .build();
          }
       );

       SampleValueObject sampleValueObj = sampleValCalc.calcSampleValue(mainObj);
       double sampleValueX = sampleValueObj.getSampleValueX(); // 此处抛出NullPointerException

       if (sampleValueX > 100) {
          return sample -> false;
       }
       
       return sample -> {
          
            if (...) {
              return false;
            }

            return true;
       };
    }
}

SampleValueCalculator类的calcSampleValue()方法实现

@RequiredArgsConstructor
public class SampleValueCalculator {

    private final SampleParameterManager sampleParameterManager;
    
    public SampleValueObject calcSampleValue(MainObj mainObj) {
        return SampleValueObject.builder()
            .sampleValueA(mainObj.getA() * sampleParameterManager.getParameter(SampleParameter.VALUE_A_FACTOR))
            .sampleValueB(mainObj.getB())
            .sampleValueC(mainObj.getC())
            .build();
    }
}

单元测试类IsSampleTrueTest

public class IsSampleTrueTest {
    private static final double SAMPLE_CONSTANT = 0.1;
    @Mock
    private SampleValueCalculator sampleValCalc;
    @Mock
    private SampleParameterManager sampleParameterManager;
    @Mock
    private Cache<String, SampleData> sampleCache;
    @Mock
    private SampleContext ctx;
    @Mock
    private MainObj mainObj;

    private IsSampleTrue predicateFunc;

    @Before
    public void before() {
        MockitoAnnotations.initMocks(this);
        when(sampleParameterManager.getParameter(SampleParameter.SAMPLE_CONSTANT_VALUE)).thenReturn(SAMPLE_CONSTANT);
        when(ctx.getC()).thenReturn(c);

        predicateFunc = new IsSampleTrue(sampleValCalc, sampleParameterManager, sampleCache);
    }

    @Test
    public void testMainObjAbsentFromContext() {
        SampleData abcd = mock(SampleData.class);
        when(sampleCache.get("abcd")).thenReturn(Optional.of(abcd));
        when(abcd.getSomeValue()).thenReturn(1.0);
        
        when(ctx.getMainObj()).thenReturn(Optional.empty());
        when(ctx.getA()).thenReturn("a");
        when(ctx.getB()).thenReturn("b");
        when(ctx.getC()).thenReturn("c");
        
        Sample s1 = setupSample(...);
        
        SampleValueObject sampleValueObj = SampleValueObject.builder()
            .sampleValueA(1.0)
            .build();
        when(sampleValueCalc.calcSampleValue(mainObj)).thenReturn(sampleValueObj);

        SamplePredicate samplePred = predicateFunc.buildSamplePredicate(ctx); // 此处抛出NullPointerException
        assertTrue(samplePred.test(sample));
    }

    @Test
    public void testMainObjPresent() {
        SampleData abcd = mock(SampleData.class);
        when(sampleCache.get("abcd")).thenReturn(Optional.of(abcd));
        when(abcd.getSomeValue()).thenReturn(1.0);
        
        when(ctx.getMainObj()).thenReturn(Optional.of(mainObj));
        
        Sample s1 = setupSample(...);
        
        SampleValueObject sampleValueObj = SampleValueObject.builder()
            .sampleValueA(1.0)
            .build();
        when(sampleValueCalc.calcSampleValue(mainObj)).thenReturn(sampleValueObj);

        SamplePredicate samplePred = predicateFunc.buildSamplePredicate(ctx);
        assertTrue(samplePred.test(sample));
    }
}

异常信息

java.lang.NullPointerException
    at com.package.sample.IsSampleTrue.buildSamplePredicate(IsSamplePredicate.java:21)
    at com.package.IsSampleTrueTest.testMainObjAbsentFromContext(IsSampleTruePredicateTest.java:42)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    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:52)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    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.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

问题原因及解决方法

核心问题

在testMainObjAbsentFromContext测试中,当上下文没有MainObj时,代码会手动构建一个真实的MainObject实例,但你给sampleValCalc设置的Mock期望是:仅当传入Mock的mainObj实例时才返回sampleValueObj。手动构建的真实实例和Mock实例不是同一个对象,所以Mockito不会返回预设的sampleValueObj,导致sampleValCalc.calcSampleValue(mainObj)返回null,最终调用getSampleValueX()时抛出空指针异常。

另外还有两个次要问题:

  1. 手动构建的MainObject中,getA()返回的是字符串"a",但SampleValueCalculator中会直接用它乘以数值,会触发类型转换错误;
  2. SampleValueObject只设置了sampleValueA,但代码中调用的是getSampleValueX(),如果这个方法依赖未初始化的字段,也可能引发空指针。

修复步骤

  1. 调整Mock期望,匹配任意MainObject实例:
    将测试中when(sampleValueCalc.calcSampleValue(mainObj)).thenReturn(sampleValueObj);改为:

    when(sampleValCalc.calcSampleValue(any(MainObject.class))).thenReturn(sampleValueObj);
    

    这样无论传入手动构建的真实实例还是Mock实例,都会返回预设的sampleValueObj。

  2. 修复MainObject字段类型不匹配问题:
    如果MainObject的a字段是数值类型,那么在测试中要返回数值而非字符串:

    when(ctx.getA()).thenReturn(1.0); // 假设a是double类型
    

    如果a确实是字符串,需要在SampleValueCalculator中先做类型转换,避免运行时错误。

  3. 确保SampleValueObject包含sampleValueX字段:
    构建SampleValueObject时,补充sampleValueX的设置:

    SampleValueObject sampleValueObj = SampleValueObject.builder()
        .sampleValueA(1.0)
        .sampleValueX(50.0) // 设置一个符合逻辑的数值
        .build();
    
  4. 可选:验证手动构建的MainObject逻辑:
    如果需要验证手动构建的MainObject的字段是否正确,可以在测试中添加断言,或者用Mockito的ArgumentCaptor捕获传入calcSampleValue的实例,检查字段值:

    ArgumentCaptor<MainObject> mainObjCaptor = ArgumentCaptor.forClass(MainObject.class);
    verify(sampleValCalc).calcSampleValue(mainObjCaptor.capture());
    MainObject capturedMainObj = mainObjCaptor.getValue();
    assertEquals("a", capturedMainObj.getA());
    assertEquals("b", capturedMainObj.getB());
    assertEquals("c", capturedMainObj.getC());
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 21:45:04