Spring Boot 3.4.x迁移@MockitoBean后Jenkins测试报空指针问题
问题:Jenkins中@MockitoSpyBean注入为空导致NullInsteadOfMock错误
环境信息
- Spring Boot 3.4.2
- TestNG 7.10.2
- Spring Test 6.2.2(父依赖引入)
问题背景
Spring Boot 3.2.0后@MockBean和@SpyBean被标记弃用,3.4.0后正式移除,因此迁移为@MockitoBean和@MockitoSpyBean。本地测试可正常运行,但Jenkins执行测试时抛出NullInsteadOfMock错误,提示传入when()的对象为null。
测试类层级
MyTest ← SomeBaseAPITest ← BaseAPITest(含自定义@DefaultTestConfig注解)← AbstractTestNGSpringContextTests
- BaseAPITest:负责加载Spring上下文
- SomeBaseAPITest:负责创建Mock并配置Stub
- MyTest:具体测试类
关键代码
自定义测试配置注解
@Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootTest(classes = { Application.class }, properties = { "spring.main.allow-bean-definition-overriding=true" }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ComponentScan({ "com.blabla.blabla" }) @TestConfiguration public @interface DefaultTestConfig { }
基础API测试类
@DefaultTestConfig public abstract class BaseAPITest extends AbstractTestNGSpringContextTests { @LocalServerPort private int port; @BeforeSuite(alwaysRun = true) public final void prepareTestContext() throws Exception { log.info("Preparing test context"); super.springTestContextPrepareTestInstance(); } }
Mock配置类
@SuppressWarnings("unchecked") public class SomeBaseAPITest extends BaseAPITest { @MockitoSpyBean(reset = MockReset.NONE) private SomeCsvServiceImpl someCsvService; @BeforeSuite(alwaysRun = true, dependsOnMethods = "prepareTestContext") protected void prepareMocks() { doAnswer(invocationOnMock -> TestService.translate(invocationOnMock.getArgument(1))) .when(morningstarCsvService) // 此处someCsvService为null .translate(any(), any(), any(), any(), any()); } }
具体测试类
public class MyTest extends SomeBaseAPITest { @Test void myTest() { var response = given() .get("http://blablabla.com"); assertTrue(response != null); } }
错误信息
18:11:09 [INFO] 18:11:09 [INFO] Results: 18:11:09 [INFO] 18:11:09 [ERROR] Failures: 18:11:09 [ERROR] SomeBaseAPITest.prepareMocks:83 » NullInsteadOfMock 18:11:09 Argument passed to when() is null! 18:11:09 Example of correct stubbing: 18:11:09 doThrow(new RuntimeException()).when(mock).someMethod(); 18:11:09 Also, if you use @Mock annotation don't miss openMocks() 18:11:09 [INFO] 18:11:09 [ERROR] Tests run: 357, Failures: 1, Errors: 0, Skipped: 356
Jenkins流水线脚本(简化版)
import hudson.util.Secret node('maven38'){ try { stage('Checkout code') { git(branch: '$BRANCH', credentialsId: 'GITHUB', url: 'https://NDA') } stage('Run Test') { withCredentials([usernamePassword(credentialsId: 'DEVOPS', passwordVariable: 'NEXUS_PASS', usernameVariable: 'NEXUS_USER')]) { sh ''' mvn --settings ${WORKSPACE}/settings.xml -Dspring.profiles.active=test -Dsurefire.suiteXmlFiles=src/test/resources/suites/api-testng.xml clean test ''' } } } catch (e) { echo 'This will run only if failed' throw e } finally { ...
已尝试无效方案
- 将私有字段改为protected
- 将Mock字段移至具体测试类
- 调整配置注解组合
解决方案
1. 调整TestNG生命周期注解,替换@BeforeSuite为@BeforeClass
TestNG的@BeforeSuite在整个测试套件启动时执行,此时Spring上下文可能尚未完成依赖注入(即使依赖prepareTestContext方法)。改为@BeforeClass可确保在当前测试类的Spring上下文完全初始化后再执行Mock的Stub配置:
@SuppressWarnings("unchecked") public class SomeBaseAPITest extends BaseAPITest { @MockitoSpyBean(reset = MockReset.NONE) private SomeCsvServiceImpl someCsvService; @BeforeClass(alwaysRun = true) protected void prepareMocks() { doAnswer(invocationOnMock -> TestService.translate(invocationOnMock.getArgument(1))) .when(someCsvService) .translate(any(), any(), any(), any(), any()); } }
同时修改BaseAPITest中的prepareTestContext为@BeforeClass,避免上下文初始化时机不匹配:
@DefaultTestConfig public abstract class BaseAPITest extends AbstractTestNGSpringContextTests { @LocalServerPort private int port; @BeforeClass(alwaysRun = true) public final void prepareTestContext() throws Exception { log.info("Preparing test context"); super.springTestContextPrepareTestInstance(); } }
2. 使用Spring的@PostConstruct替代TestNG的生命周期注解
利用Spring的依赖注入完成后触发的@PostConstruct注解,确保Mock对象注入后再配置Stub:
@SuppressWarnings("unchecked") public class SomeBaseAPITest extends BaseAPITest { @MockitoSpyBean(reset = MockReset.NONE) private SomeCsvServiceImpl someCsvService; @PostConstruct protected void prepareMocks() { doAnswer(invocationOnMock -> TestService.translate(invocationOnMock.getArgument(1))) .when(someCsvService) .translate(any(), any(), any(), any(), any()); } }
此方案无需依赖TestNG的生命周期顺序,完全由Spring控制时机,避免环境差异导致的问题。
3. 检查Jenkins Maven命令的并行配置
若Jenkins中Maven Surefire插件启用了并行测试(如forkCount、parallel参数),可能导致上下文初始化和Mock注入不同步。可在Maven命令中添加参数禁用并行:
mvn --settings ${WORKSPACE}/settings.xml -Dspring.profiles.active=test -Dsurefire.suiteXmlFiles=src/test/resources/suites/api-testng.xml -Dsurefire.parallel=none clean test
内容的提问来源于stack exchange,提问作者Ivan Patiuk
相关产品推荐
相关产品推荐

