Spring Boot Kotlin测试中save(...)不能为null问题排查求助
Spring Boot + Kotlin 单元测试NullPointerException问题排查
问题描述
基于Spring Boot 3.2.4 + Kotlin的简易CRUD应用中,其他实体及仓库的单元测试均正常,但User实体(与Team实体关联)的createUser测试抛出NullPointerException,错误提示为save(...) must not be null。
相关代码
实体类
@Entity @Table(name = "\"user\"") @DynamicUpdate class UserEntity( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_generator") @SequenceGenerator(name = "user_generator", sequenceName = "user_id_seq", allocationSize = 1) @Column(name = "id") var userId: Int?, @Column(name = "email") val email: String, @Enumerated(EnumType.STRING) @Column(name = "role", columnDefinition = "user_role_enum", nullable = false) val role: Role, @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "user_teams_team", joinColumns = [JoinColumn(name = "user_id")], inverseJoinColumns = [JoinColumn(name = "team_id")] ) val teams: Set<TeamEntity>? = null ) @Entity @Table(name = "team") @DynamicUpdate class TeamEntity( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "team_generator") @SequenceGenerator(name = "team_generator", sequenceName = "team_id_seq", allocationSize = 1) @Column(name = "id") var teamId: Int?, @Column(name = "name_ru") val nameRu: String, @Column(name = "name_en") val nameEn: String, @Column(name = "name_kk") val nameKk: String )
DTO
class UserDto( val email: String, val role: Role, val teams: List<Int>? ) data class UserResponseDto( val userId: Int, val email: String, val role: Role, var teams: Set<TeamEntity>? )
UserServiceImpl
package kt.cell.jobsapi.service.impl import kt.cell.jobsapi.exception.InternalServerException import kt.cell.jobsapi.exception.NotFoundException import kt.cell.jobsapi.model.dto.* import kt.cell.jobsapi.model.mapper.UserMapper import kt.cell.jobsapi.repository.UserRepository import kt.cell.jobsapi.service.UserService import org.springframework.dao.DataIntegrityViolationException import org.springframework.stereotype.Service import java.util.* @Service class UserServiceImpl( val userRepository: UserRepository, val userMapper: UserMapper, val teamServiceImpl: TeamServiceImpl ) : UserService { @Throws(NotFoundException::class) override fun getUsers(): List<UserResponseDto> { val userEntityList = userRepository.findAll() return if (userEntityList.isNotEmpty()) { userEntityList.map { userMapper.toDto(it) } } else { throw NotFoundException("No users found") } } @Throws(Exception::class, DataIntegrityViolationException::class, NotFoundException::class) override fun createUser(userDto: UserDto): UserResponseDto { teamServiceImpl.checkIfSuchTeamsExistOrThrowNotFoundException(userDto) val teamSet = teamServiceImpl.createTeamSetFromTeamIdsOrThrowNotFoundException(userDto) val savedUser = userRepository.save(userMapper.toEntity(userDto, teamSet)) return userMapper.toDto(savedUser) }
TeamServiceImpl相关方法
fun createTeamSetFromTeamIdsOrThrowNotFoundException(userDto: UserDto): MutableSet<TeamEntity> { val teamSet: MutableSet<TeamEntity> = mutableSetOf() userDto.teams?.forEach { teamId -> val teamEntity = teamRepository.findById(teamId) .orElseThrow { NotFoundException("There is no team with id: $teamId") } teamSet.add(teamEntity) } return teamSet } fun checkIfSuchTeamsExistOrThrowNotFoundException(userDto: UserDto) { val existingInDbTeamIdList = teamRepository.findAll().map { it.teamId!! } userDto.teams?.forEach { teamId -> if (teamId !in existingInDbTeamIdList) { throw NotFoundException("There is no team with id: $teamId") } } }
UserServiceImplTests
@ExtendWith(SpringExtension::class) @TestInstance(TestInstance.Lifecycle.PER_METHOD) class UserServiceImplTests { @Mock private lateinit var teamMapper: TeamMapper @Mock private lateinit var userRepository: UserRepository @Mock private lateinit var teamRepository: TeamRepository @Mock private lateinit var teamService: TeamServiceImpl @Mock private lateinit var userMapper: UserMapper @InjectMocks private lateinit var userService: UserServiceImpl @Captor private lateinit var userEntityCaptor: ArgumentCaptor<UserEntity> @Test fun createUser_Success() { val id = 1 val teamEntity = TeamEntity(id, "testRu", "testEn", "testKk") val userEntity = UserEntity(id, "test", Role.OWNER, setOf(teamEntity)) val userEntity1 = UserEntity(id, "test", Role.OWNER, setOf(teamEntity)) val teamSet = mutableSetOf(teamEntity) val userDto = UserDto("test", Role.OWNER, listOf(1)) val userResponseDto = UserResponseDto(id, "test", Role.OWNER, setOf(teamEntity)) whenever(teamRepository.findAll()).thenReturn(listOf(teamEntity)) whenever(teamRepository.findById(id)).thenReturn(Optional.of(teamEntity)) whenever(userMapper.toEntity(userDto, teamSet)).thenReturn(userEntity) whenever(userRepository.save(userEntity)).thenReturn(userEntity) whenever(userMapper.toDto(userEntity)).thenReturn(userResponseDto) val result = userService.createUser(userDto) assertEquals(userResponseDto, result) verify(userRepository).save(userEntityCaptor.capture()) val capturedEntity = userEntityCaptor.value assertNotNull(capturedEntity) assertEquals(userEntity, capturedEntity) verify(userRepository).save(userEntity) verify(teamRepository).findAll() }
错误信息
save(...) must not be null java.lang.NullPointerException: save(...) must not be null at kt.cell.jobsapi.service.impl.UserServiceImpl.createUser(UserServiceImpl.kt:39) at kt.cell.jobsapi.service.impl.UserServiceImplTests.createUser_Success(UserServiceImplTests.kt:123)
问题原因与解决方法
原因分析
测试中通过@Mock创建了teamService(即UserServiceImpl依赖的teamServiceImpl),但未对其核心方法进行Stub配置:
- Mock对象的非void方法默认返回
null,导致createTeamSetFromTeamIdsOrThrowNotFoundException返回null - 测试中仅对
userMapper.toEntity(userDto, teamSet)(手动创建的集合)做了Mock匹配,但实际调用时传入的是null,导致userMapper.toEntity返回null - 最终调用
userRepository.save(null)触发NPE,因为Spring Data JPA的save方法不接受null参数
解决方法
在测试的Mock配置块中,为teamServiceImpl的两个方法添加Stub配置,确保返回预期结果:
// 新增以下两行配置 whenever(teamServiceImpl.checkIfSuchTeamsExistOrThrowNotFoundException(userDto)).doNothing() whenever(teamServiceImpl.createTeamSetFromTeamIdsOrThrowNotFoundException(userDto)).thenReturn(teamSet)
同时可清理冗余代码,比如删除重复的verify(userRepository).save(userEntity),保留userEntityCaptor相关的验证即可。
内容的提问来源于stack exchange,提问作者matrixsh1t
相关产品推荐
相关产品推荐

