Spring中使用Mockito测试Service层遇空指针及自定义异常问题
Mockito单元测试实例为空、断言非空失败的排查方案
问题背景
使用Mockito模拟Repository和Service,通过when().thenReturn()配置返回值并配合verify做测试,但调用findById(2L)时,模拟实例抛出自定义异常,测试中断言结果非空失败,相关代码及配置如下:
测试类代码(GameCategoryServiceUnitTest.java)
@ExtendWith(MockitoExtension.class) class GameCategoryServiceUnitTest { public static final String CATEGORY_NAME = "detective"; @Mock private GameCategoryRepository gameCategoryRepository; @InjectMocks private GameCategoryService gameCategoryService; @Mock private GameCategoryMapper gameCategoryMapper; GameCategory existingGameCategory; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); existingGameCategory = GameCategory.builder() .id(1L) .name(CATEGORY_NAME) .build(); } @Test void whenCallingFindById_shouldReturnGameCategory() { String name = "cooperative"; GameCategory gameCategory = GameCategory.builder() .id(2L) .name(name).build(); GameCategoryDto gameCategoryDto = GameCategoryDto.builder() .name(name) .id(2L) .build(); when(gameCategoryRepository.findById(2L)).thenReturn(Optional.of(gameCategory)); // When GameCategoryDto result = gameCategoryService.findById(2L); //then assertNotNull(result); assertEquals(name, result.getName()); verify(gameCategoryRepository).findById(2L); // Verify that the findById method was called } }
测试配置(application-test.yml)
spring: datasource: url: jdbc:h2:mem/unit_test_db username: sa password: jpa: database: H2 database-platform: org.hibernate.dialect.H2Dialect show-sql: true hibernate: ddl-auto: create-drop test: database: replace: none
补充业务类代码
GameCategoryService.java
import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Slf4j public class GameCategoryService { @Autowired private GameCategoryRepository gameCategoryRepository; private GameCategoryMapper mapper; public List<GameCategoryDto> findAll() { return gameCategoryRepository.findAll().stream() .map(mapper::fromGameCategoryToGameCategoryDto) .collect(Collectors.toList()); } public GameCategoryDto findById(long gameCategoryId) { GameCategory gameCategory = gameCategoryRepository.findById(gameCategoryId).orElseThrow(() - new EntityNotFoundException(EntityNotFoundException.ENTITY_GAME_CATEGORY_NOT_FOUND_BY_ID + gameCategoryId)); return mapper.fromGameCategoryToGameCategoryDto(gameCategory); } public GameCategoryDto findByName(String name) { GameCategory gameCategory = gameCategoryRepository.findByName(name).orElseThrow(() - new EntityNotFoundException(EntityNotFoundException.ENTITY_GAME_CATEGORY_NOT_FOUND_BY_NAME, name)); return mapper.fromGameCategoryToGameCategoryDto(gameCategory); } public GameCategoryDto createGameCategory(GameCategoryUpdateDto gameCategoryUpdateDto) { if (gameCategoryRepository.findByName(gameCategoryUpdateDto.getName()).isPresent()) { throw new EntityAlreadyExistsException(EntityAlreadyExistsException.GAME_CATEGORY_AlREADY_EXISTS_MESSAGE, gameCategoryUpdateDto.getName()); } GameCategory gameCategory = mapper.fromGameCategoryDtoToGameCategory(gameCategoryUpdateDto); GameCategory savedGameCategory = gameCategoryRepository.save(gameCategory); return mapper.fromGameCategoryToGameCategoryDto(savedGameCategory); } public GameCategoryDto updateGameCategory(long gameCategoryId, GameCategoryDto update) { GameCategory gameCategory = gameCategoryRepository.findById(gameCategoryId) .orElseThrow(() -> new EntityNotFoundException(EntityNotFoundException.ENTITY_GAME_CATEGORY_NOT_FOUND_BY_ID, String.valueOf(gameCategoryId))); GameCategory gameUpdated = mapper.fromGameCategoryDtoToGameCategory(update); GameCategory savedGame = gameCategoryRepository.save(gameUpdated); return mapper.fromGameCategoryToGameCategoryDto(savedGame); } public ResponseEntity<String> deleteGameCategory(long gameCategoryId) { GameCategory gameCategory = gameCategoryRepository.findById(gameCategoryId) .orElseThrow(() -> new EntityNotFoundException(EntityNotFoundException.ENTITY_GAME_CATEGORY_NOT_FOUND_BY_ID, String.valueOf(gameCategoryId))); gameCategoryRepository.deleteById(gameCategoryId); return ResponseEntity .status(HttpStatus.OK) .build(); } }
GameCategoryRepository.java
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface GameCategoryRepository extends JpaRepository<GameCategory, Long> { Optional<GameCategory> findByName(String name); Optional<GameCategory> findById(long id); }
错误栈信息
org.opentest4j.AssertionFailedError: expected: not <null> at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152) at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132) at org.junit.jupiter.api.AssertNotNull.failNull(AssertNotNull.java:49) at org.junit.jupiter.api.AssertNotNull.assertNotNull(AssertNotNull.java:35) at org.junit.jupiter.api.AssertNotNull.assertNotNull(AssertNotNull.java:30) at org.junit.jupiter.api.Assertions.assertNotNull(Assertions.java:304) at com.adaland.springsecurity.service.GameCategoryServiceUnitTest.whenCallingFindById_shouldReturnGameCategory(GameCategoryServiceUnitTest.java:61) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
排查修复点
- 修复Mapper注入问题:Service中的
GameCategoryMapper mapper字段既无@Autowired注解,也不是final类型(无法被@RequiredArgsConstructor自动构造注入),导致Mockito的@InjectMocks无法将Mock的mapper注入进去。解决方式:要么给mapper加@Autowired,要么改为final GameCategoryMapper mapper。 - 补充Mapper的Mock配置:测试中仅Mock了Repository的返回,但Service调用了
mapper.fromGameCategoryToGameCategoryDto()方法,未配置该Mock的返回值会默认返回null,直接导致findById返回null。需在测试中添加:when(gameCategoryMapper.fromGameCategoryToGameCategoryDto(gameCategory)).thenReturn(gameCategoryDto); - 移除重复初始化代码:测试类已加
@ExtendWith(MockitoExtension.class),该注解会自动初始化Mock实例,无需在setUp中重复调用MockitoAnnotations.openMocks(this);,重复调用可能导致实例重置为空。 - 统一参数匹配类型:Repository的
findById参数为long,测试中传2L(Long类型)虽能自动拆箱,但建议用anyLong()匹配或保持参数类型一致,避免匹配失效。 - 确保纯单元测试上下文:
application-test.yml是集成测试配置,纯单元测试无需加载Spring上下文,不要添加@SpringBootTest等注解,防止Mockito实例被Spring真实实例覆盖。
内容的提问来源于stack exchange,提问作者Hekima
相关产品推荐
相关产品推荐

