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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 10:12:08