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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 12:35:06