如何用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()时抛出空指针异常。
另外还有两个次要问题:
- 手动构建的
MainObject中,getA()返回的是字符串"a",但SampleValueCalculator中会直接用它乘以数值,会触发类型转换错误; SampleValueObject只设置了sampleValueA,但代码中调用的是getSampleValueX(),如果这个方法依赖未初始化的字段,也可能引发空指针。
修复步骤
调整Mock期望,匹配任意
MainObject实例:
将测试中when(sampleValueCalc.calcSampleValue(mainObj)).thenReturn(sampleValueObj);改为:when(sampleValCalc.calcSampleValue(any(MainObject.class))).thenReturn(sampleValueObj);这样无论传入手动构建的真实实例还是Mock实例,都会返回预设的
sampleValueObj。修复
MainObject字段类型不匹配问题:
如果MainObject的a字段是数值类型,那么在测试中要返回数值而非字符串:when(ctx.getA()).thenReturn(1.0); // 假设a是double类型如果
a确实是字符串,需要在SampleValueCalculator中先做类型转换,避免运行时错误。确保
SampleValueObject包含sampleValueX字段:
构建SampleValueObject时,补充sampleValueX的设置:SampleValueObject sampleValueObj = SampleValueObject.builder() .sampleValueA(1.0) .sampleValueX(50.0) // 设置一个符合逻辑的数值 .build();可选:验证手动构建的
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

