
本文详解如何在 Spring Boot 单元测试中正确模拟依赖、处理 Optional.map().orElseGet() 链式调用,防止因 userMapper.toEntity(null) 返回 null 导致 NPE。
本文详解如何在 spring boot 单元测试中正确模拟依赖、处理 optional.map().orelseget() 链式调用,防止因 usermapper.toentity(null) 返回 null 导致 npe。
在您提供的测试代码中,NullPointerException 并非源于 updatedUserDto 本身被注入失败,而是根本性逻辑缺陷:userMapper.toEntity(userDto) 在测试中未被正确 stub(打桩),导致其返回 null;当后续调用 .setId(...) 时即触发空指针。
关键问题在于:
✅ userMapper 是真实对象还是 Mock?—— 您的测试中未声明 @Mock private UserMapper userMapper;,也未对 toEntity() 方法进行 stub;
✅ userDto.getUserId() 返回值为 null —— 因为 userDto.setId(...) 设置的是 id 字段,但 userId 字段未赋值;而 userRepository.findByUserIdAndType(...) 的第一个参数实际传入 null,导致 findByUserIdAndType(null, ACTIF) 可能返回 Optional.empty(),进而进入 orElseGet 分支,再次调用 userMapper.toEntity(userDto)(仍为 null)→ userRepository.save(null) → NPE。
✅ 正确修复步骤
1. 补全 Mock 声明与依赖注入
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@InjectMocks
private UserService service; // 被测服务(自动注入所有 @Mock)
@Mock
private UserRepository userRepository;
@Mock
private UserMapper userMapper; // 必须显式声明并 mock!
2. 严格 stub userMapper.toEntity() 和 toDto()
@Test
void whenCreateOrUpdateUser_thenReturnsUpdatedDto() {
// Given
UUID userId = UUID.randomUUID();
UserDto userDto = new UserDto();
userDto.setId(userId);
userDto.setName("toto");
// ⚠️ 关键:显式设置 userId(若 DTO 中有该字段),否则 findByUserIdAndType 会传 null
userDto.setUserId(userId); // ← 此行常被遗漏!
UserEntity existingEntity = new UserEntity();
existingEntity.setId(1L);
existingEntity.setName("old");
UserEntity savedEntity = new UserEntity();
savedEntity.setId(1L);
savedEntity.setName("toto");
// Stub mapper: toEntity 必须返回非 null 实体
when(userMapper.toEntity(userDto)).thenReturn(savedEntity); // ← 核心修复!
when(userMapper.toDto(savedEntity)).thenReturn(userDto);
// Stub repository: 注意匹配 actual 参数(userId 不为 null)
when(userRepository.findByUserIdAndType(userId, UserTypes.ACTIF))
.thenReturn(Optional.of(existingEntity));
when(userRepository.save(any(UserEntity.class))).thenReturn(savedEntity);
// When
UserDto result = service.createOrUpdateUser(userDto);
// Then
assertEquals(userDto, result);
}
3. 补充边界测试(推荐)
验证 userId == null 或 Optional.empty() 场景:
@Test
void whenCreateOrUpdateUser_withNewUser_thenCreates() {
UserDto newUserDto = new UserDto();
newUserDto.setUserId(UUID.randomUUID()); // 确保不为 null
newUserDto.setName("new");
UserEntity mappedEntity = new UserEntity();
mappedEntity.setName("new");
mappedEntity.setId(2L);
when(userMapper.toEntity(newUserDto)).thenReturn(mappedEntity);
when(userMapper.toDto(mappedEntity)).thenReturn(newUserDto);
when(userRepository.findByUserIdAndType(any(), any())).thenReturn(Optional.empty());
when(userRepository.save(mappedEntity)).thenReturn(mappedEntity);
UserDto result = service.createOrUpdateUser(newUserDto);
assertNotNull(result);
}
⚠️ 注意事项总结
-
永远不要假设 Mapper 方法返回非 null:
toEntity()必须被when(...).thenReturn(...)明确 stub; -
DTO 字段完整性:确保
userDto.getUserId()在测试中已赋值,否则findByUserIdAndType(null, ...)极易返回Optional.empty(); -
避免
@SpringBootTest过度使用:本场景纯单元测试即可解决,引入@SpringBootTest属重量级集成测试,违背快速反馈原则; -
使用
any()匹配器需谨慎:生产代码中建议用eq(expectedId)提高可读性与断言精度; -
验证
save()参数:可用verify(userRepository).save(argThat(e -> e.getName().equals("toto")));增强测试健壮性。
通过以上结构化 mock 与数据准备,即可彻底规避 NullPointerException,构建稳定、可维护的服务层单元测试。











