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)); } }
问题分析
- 注解混用冲突:
@SpringBootTest与@InjectMocks混用,导致Spring上下文和Mockito的依赖注入逻辑冲突,无法正确注入mock的JaegerTracer。 - @SpringBootTest配置错误:
classes参数传入测试类本身而非被测试服务类,Spring无法正确加载服务Bean定义。 - 初始化顺序错误:
MockitoAnnotations.openMocks(this)应在设置字段前执行,否则mock对象未初始化,字段设置无效。 - 测试逻辑缺失:未调用目标方法
getHeroMovieDetails(),直接断言初始化的heroMovie,无法验证服务逻辑。 - 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
相关产品推荐
相关产品推荐

