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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 22:14:50