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

SpringBoot中Mockito测试QueryDSL搜索方法遇空指针异常求助

问题分析与解决方案

问题根源

你遇到的空指针异常,本质是两个问题叠加导致:

  1. 服务层直接局部实例化JPAQuery:new JPAQuery<Rental>(entityManager)这种写法,Mockito无法拦截Mock局部创建的对象,你测试中Mock的JPAQuery实例根本没被用到,服务层实际使用的是全新创建的JPAQuery实例。
  2. 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));
    }
}

关键注意点

  1. 链式调用Stub:QueryDSL的查询方法是链式调用,每个方法都会返回新的查询实例,因此需要保证每个Stub的方法都返回同一个Mock的JPAQuery对象。
  2. 元模型无需Mock:QRental是静态生成的元模型,直接使用真实实例即可,Mock它会导致查询条件构建失败。
  3. 避免PowerMock:如果不想重构服务层,有人会用PowerMock拦截new JPAQuery的调用,但这种方式会破坏JUnit的正常执行机制,不推荐使用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 15:57:02