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

Spring Boot中JaegerTracer模拟对象引发空指针异常求助

Spring Boot集成JaegerTracer的JUnit测试空指针异常解决方案

问题描述

Spring Boot服务集成Jaeger实现链路追踪,服务运行正常,但编写JUnit5测试时,JaegerTracer对象为空,导致jaegerTracer.buildSpan("getHeroMovieDetails").start()处抛出空指针异常(NPE)。使用技术版本:JUnit5、SpringBoot 2.7.6。

服务代码

@Service
public class RestClientHeroMovieService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private RestClientService restClientService;

    @Autowired
    private StatisticsConfig statisticsConfig;

    @Autowired
    private JaegerTracer jaegerTracer;

    @Value("${test.value}")
    private String testValue;

    public HeroMovie getHeroMovieDetails(String heroName) throws URISyntaxException {

        Span span = jaegerTracer.buildSpan("getHeroMovieDetails").start(); // NPE触发点

        Span spanHeroMovie = jaegerTracer.buildSpan("spanHeroMovie").asChildOf(span).start();
        String endpoint = "http://localhost:9050/orders/v1/getHeroDetails/"+heroName;

        HeroMovie heroMovie = restTemplate.getForObject(endpoint,HeroMovie.class);
        spanHeroMovie.finish();

        endpoint = "http://localhost:9050/orders/v1/getMoviesOfHero/"+heroName;
        URI uri = new URI(endpoint);

        Span spanListMovie = jaegerTracer.buildSpan("spanListMovie").asChildOf(span).start();
        List<Movie> listMovie = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference<List<Movie>>(){}).getBody();
        heroMovie.setMovies(listMovie);
        spanListMovie.finish();

        heroMovie.setPlatforms(statisticsConfig.getPlatforms());
        heroMovie.setBrowsers(statisticsConfig.getBrowsers());
        heroMovie.setSystems(statisticsConfig.getSystems());

        heroMovie.setSomeTestValue(testValue);

        heroMovie.setPet(getPet(heroMovie.getHeroName()));

        restClientService.clearAllCache();

        span.finish();
        return heroMovie;

    }
}

原JUnit测试代码

@ExtendWith({SpringExtension.class})  
@ExtendWith({MockitoExtension.class})  
@SpringBootTest(classes = {RestClientHeroMovieServiceTest.class, StatisticsConfig.class}) 
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@EnableConfigurationProperties(value = {StatisticsConfig.class})
@TestPropertySource("classpath:test.properties")
public class RestClientHeroMovieServiceTest {

    private final String testValue = "aaa";

    @InjectMocks
    @Spy
    private RestClientHeroMovieService restClientHeroMovieService;
    @Mock
    private RestTemplate restTemplate;
    @Mock
    private RestClientService restClientService;

    @Mock
    private StatisticsConfig statisticsConfig;

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private JaegerTracer jaegerTracer;

    @BeforeEach
    public void init() {
        ReflectionTestUtils.setField(restClientHeroMovieService, "testValue", testValue);
        MockitoAnnotations.openMocks(this);
    }

    @Test
    @SneakyThrows
    public void testGetHeroMovieDetails() {

        HeroMovie heroMovie = getHeroMovie();

        restClientHeroMovieService.setJaegerTracer(jaegerTracer);
        Mockito.when(jaegerTracer.buildSpan(Mockito.anyString()).start())
                .thenReturn(Mockito.mock(JaegerSpan.class));

        Mockito.doReturn(heroMovie)
                .when(restTemplate)
                .getForObject(Mockito.anyString(), Mockito.any());

        List<Movie> listMovie = getListMovie();
        ResponseEntity<List<Movie>> responseEntityListMovie = ResponseEntity.ok(listMovie);

        Mockito.doReturn(responseEntityListMovie)
                .when(restTemplate)
                .exchange(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(ParameterizedTypeReference.class));

        Mockito.doNothing()
                .when(restClientService)
                .clearAllCache();

        Assertions.assertTrue(heroMovie.getHeroName().equalsIgnoreCase("test") && heroMovie.getMovies().get(0).getMovieName().equalsIgnoreCase("TestM") && heroMovie.getSomeTestValue().equalsIgnoreCase(testValue));

    }
}

问题分析

  1. 注解混用冲突:@SpringBootTest与@InjectMocks混用,导致Spring上下文和Mockito的依赖注入逻辑冲突,无法正确注入mock的JaegerTracer。
  2. @SpringBootTest配置错误:classes参数传入测试类本身而非被测试服务类,Spring无法正确加载服务Bean定义。
  3. 初始化顺序错误:MockitoAnnotations.openMocks(this)应在设置字段前执行,否则mock对象未初始化,字段设置无效。
  4. 测试逻辑缺失:未调用目标方法getHeroMovieDetails(),直接断言初始化的heroMovie,无法验证服务逻辑。
  5. JaegerSpan mock不兼容:直接mock Jaeger内部实现类JaegerSpan可能存在类型转换问题,应mock标准Span接口。

解决方案

提供两种可行方案,根据测试场景选择:

方案一:纯Mockito单元测试(无Spring上下文)

去掉Spring相关注解,仅用Mockito管理依赖,适合快速单元测试:

@ExtendWith(MockitoExtension.class)
public class RestClientHeroMovieServiceTest {

    private final String testValue = "aaa";

    @InjectMocks
    private RestClientHeroMovieService restClientHeroMovieService;

    @Mock
    private RestTemplate restTemplate;
    @Mock
    private RestClientService restClientService;
    @Mock
    private StatisticsConfig statisticsConfig;
    @Mock
    private JaegerTracer jaegerTracer;
    @Mock
    private Span span; // mock标准Span接口

    @BeforeEach
    public void init() {
        MockitoAnnotations.openMocks(this);
        ReflectionTestUtils.setField(restClientHeroMovieService, "testValue", testValue);
        // 预定义JaegerTracer的mock行为
        when(jaegerTracer.buildSpan(anyString())).thenReturn(mock(SpanBuilder.class, RETURNS_DEEP_STUBS));
        when(jaegerTracer.buildSpan(anyString()).start()).thenReturn(span);
    }

    @Test
    @SneakyThrows
    public void testGetHeroMovieDetails() {
        // 准备测试数据
        HeroMovie heroMovie = getHeroMovie();
        List<Movie> listMovie = getListMovie();
        ResponseEntity<List<Movie>> responseEntityListMovie = ResponseEntity.ok(listMovie);

        // mock依赖行为
        when(restTemplate.getForObject(anyString(), eq(HeroMovie.class))).thenReturn(heroMovie);
        when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), isNull(), any(ParameterizedTypeReference.class)))
                .thenReturn(responseEntityListMovie);
        doNothing().when(restClientService).clearAllCache();

        // 调用目标方法
        HeroMovie result = restClientHeroMovieService.getHeroMovieDetails("test");

        // 断言结果
        assertNotNull(result);
        assertTrue(result.getHeroName().equalsIgnoreCase("test"));
        assertTrue(result.getMovies().get(0).getMovieName().equalsIgnoreCase("TestM"));
        assertTrue(result.getSomeTestValue().equalsIgnoreCase(testValue));
        // 验证Span的finish方法调用次数
        verify(span, times(3)).finish();
    }

    // 补充测试数据构造方法
    private HeroMovie getHeroMovie() {
        HeroMovie hero = new HeroMovie();
        hero.setHeroName("test");
        return hero;
    }

    private List<Movie> getListMovie() {
        Movie movie = new Movie();
        movie.setMovieName("TestM");
        return Collections.singletonList(movie);
    }
}

方案二:Spring环境集成测试

如果需要在Spring上下文环境中测试,使用@MockBean让Spring管理mock对象:

@SpringBootTest(classes = RestClientHeroMovieService.class)
@EnableConfigurationProperties(StatisticsConfig.class)
@TestPropertySource("classpath:test.properties")
public class RestClientHeroMovieServiceTest {

    private final String testValue = "aaa";

    @Autowired
    private RestClientHeroMovieService restClientHeroMovieService;

    @MockBean
    private RestTemplate restTemplate;
    @MockBean
    private RestClientService restClientService;
    @MockBean
    private StatisticsConfig statisticsConfig;
    @MockBean
    private JaegerTracer jaegerTracer;
    @Mock
    private Span span;

    @BeforeEach
    public void init() {
        ReflectionTestUtils.setField(restClientHeroMovieService, "testValue", testValue);
        when(jaegerTracer.buildSpan(anyString())).thenReturn(mock(SpanBuilder.class, RETURNS_DEEP_STUBS));
        when(jaegerTracer.buildSpan(anyString()).start()).thenReturn(span);
    }

    @Test
    @SneakyThrows
    public void testGetHeroMovieDetails() {
        HeroMovie heroMovie = getHeroMovie();
        List<Movie> listMovie = getListMovie();
        ResponseEntity<List<Movie>> responseEntityListMovie = ResponseEntity.ok(listMovie);

        when(restTemplate.getForObject(anyString(), eq(HeroMovie.class))).thenReturn(heroMovie);
        when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), isNull(), any(ParameterizedTypeReference.class)))
                .thenReturn(responseEntityListMovie);
        doNothing().when(restClientService).clearAllCache();

        HeroMovie result = restClientHeroMovieService.getHeroMovieDetails("test");

        assertNotNull(result);
        assertTrue(result.getHeroName().equalsIgnoreCase("test"));
        assertTrue(result.getMovies().get(0).getMovieName().equalsIgnoreCase("TestM"));
        assertTrue(result.getSomeTestValue().equalsIgnoreCase(testValue));
        verify(span, times(3)).finish();
    }

    // 补充测试数据构造方法
    private HeroMovie getHeroMovie() {
        HeroMovie hero = new HeroMovie();
        hero.setHeroName("test");
        return hero;
    }

    private List<Movie> getListMovie() {
        Movie movie = new Movie();
        movie.setMovieName("TestM");
        return Collections.singletonList(movie);
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 09:16:04