SpringBoot中Mockito测试QueryDSL搜索方法遇空指针异常求助
问题分析与解决方案
问题根源
你遇到的空指针异常,本质是两个问题叠加导致:
- 服务层直接局部实例化
JPAQuery:new JPAQuery<Rental>(entityManager)这种写法,Mockito无法拦截Mock局部创建的对象,你测试中Mock的JPAQuery实例根本没被用到,服务层实际使用的是全新创建的JPAQuery实例。 - Mock的
EntityManager未Stub核心依赖:新创建的JPAQuery会调用EntityManager.getEntityManagerFactory(),但你没有为Mock的EntityManagerStub这个方法,导致返回null触发NPE。
另外,QRental是QueryDSL生成的元模型静态实例,不需要Mock,Mock它反而会破坏后续的查询条件构建。
解决方案:重构依赖+正确Mock
第一步:重构服务层,改用JPAQueryFactory注入
将直接创建JPAQuery的逻辑替换为注入JPAQueryFactory,这样测试时可以完全Mock查询逻辑,无需依赖真实的EntityManager:
@Service public class RentalServiceImpl implements RentalService { private final JPAQueryFactory queryFactory; private final RentalMapper rentalMapper; // 构造注入JPAQueryFactory和映射器 @Autowired public RentalServiceImpl(JPAQueryFactory queryFactory, RentalMapper rentalMapper) { this.queryFactory = queryFactory; this.rentalMapper = rentalMapper; } public List<RentalDto> searchRentals(String reason) { QRental qRental = QRental.rental; JPAQuery<Rental> jpaQuery = queryFactory.select(qRental) .from(qRental); if (reason != null && !reason.isBlank()) { jpaQuery = jpaQuery.where(qRental.reason.likeIgnoreCase(reason)); } List<Rental> result = jpaQuery.fetch(); return rentalMapper.toDtos(result); } }
第二步:编写正确的测试代码
MockJPAQueryFactory和JPAQuery,Stub链式调用返回预定义数据:
@ExtendWith(MockitoExtension.class) class RentalServiceImplTest { @Mock private JPAQueryFactory queryFactory; @Mock private JPAQuery<Rental> jpaQuery; @Mock private RentalMapper rentalMapper; @InjectMocks private RentalServiceImpl rentalService; @Test void searchRentals_withReason_returnsFilteredDtos() { // 1. 定义测试数据 String testReason = "test"; Rental testRental = new Rental(); testRental.setReason(testReason); List<Rental> mockRentalList = List.of(testRental); RentalDto testDto = new RentalDto(); List<RentalDto> mockDtoList = List.of(testDto); // 2. Stub QueryDSL链式调用 QRental qRental = QRental.rental; when(queryFactory.select(qRental)).thenReturn(jpaQuery); when(jpaQuery.from(qRental)).thenReturn(jpaQuery); when(jpaQuery.where(qRental.reason.likeIgnoreCase(testReason))).thenReturn(jpaQuery); when(jpaQuery.fetch()).thenReturn(mockRentalList); // 3. Stub映射器转换 when(rentalMapper.toDtos(mockRentalList)).thenReturn(mockDtoList); // 4. 执行测试并验证 List<RentalDto> result = rentalService.searchRentals(testReason); assertEquals(mockDtoList, result); // 验证调用逻辑符合预期 verify(queryFactory).select(qRental); verify(jpaQuery).from(qRental); verify(jpaQuery).where(qRental.reason.likeIgnoreCase(testReason)); verify(jpaQuery).fetch(); } @Test void searchRentals_withoutReason_returnsAllDtos() { // 定义测试数据 List<Rental> mockRentalList = List.of(new Rental(), new Rental()); List<RentalDto> mockDtoList = List.of(new RentalDto(), new RentalDto()); // Stub QueryDSL调用 QRental qRental = QRental.rental; when(queryFactory.select(qRental)).thenReturn(jpaQuery); when(jpaQuery.from(qRental)).thenReturn(jpaQuery); when(jpaQuery.fetch()).thenReturn(mockRentalList); // Stub映射器 when(rentalMapper.toDtos(mockRentalList)).thenReturn(mockDtoList); // 执行测试 List<RentalDto> result = rentalService.searchRentals(null); assertEquals(mockDtoList, result); // 验证where方法未被调用 verify(jpaQuery, never()).where(any(Predicate.class)); } }
关键注意点
- 链式调用Stub:QueryDSL的查询方法是链式调用,每个方法都会返回新的查询实例,因此需要保证每个Stub的方法都返回同一个Mock的
JPAQuery对象。 - 元模型无需Mock:
QRental是静态生成的元模型,直接使用真实实例即可,Mock它会导致查询条件构建失败。 - 避免PowerMock:如果不想重构服务层,有人会用PowerMock拦截
new JPAQuery的调用,但这种方式会破坏JUnit的正常执行机制,不推荐使用。
内容的提问来源于stack exchange,提问作者Outis
相关产品推荐
相关产品推荐

