Mockito测试中H2数据库唯一约束未触发问题求助
问题
我正在编写测试确保Service层工作正常,但尝试违反username唯一约束并期望触发DataIntegrityViolationException时,该约束似乎被忽略了。
表结构
CREATE TABLE IF NOT EXISTS table_users( uuid UUID PRIMARY KEY UNIQUE, username VARCHAR UNIQUE, password VARCHAR NOT NULL, first_name VARCHAR NOT NULL, last_name VARCHAR NOT NULL, is_deleted BOOLEAN NOT NULL DEFAULT FALSE );
User实体类
@Entity(name = "table_users") data class User( @Id @GeneratedValue(strategy = GenerationType.UUID) @Column(name = "uuid") val uuid: UUID? = null, @Column(name = "username", unique = true) @NotNull(message = "Please provide a username") @NotBlank val username: String? = null, @Column(name = "password") @NotNull @NotBlank var password: String? = null, @Column(name = "first_name") @NotNull(message = "Please provide a first name.") @NotBlank var firstName: String? = null, @Column(name = "last_name") @NotNull @NotBlank var lastName: String? = null, @Column(name = "is_deleted") @NotNull var isDeleted: Boolean = false ) { // Omitted builder class for brevity }
Service层saveUser方法
override fun saveUser(user: User): DatabaseResult<User> { return try { if(user.firstName.isNullOrBlank()) { return DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = UserService.ErrorMessages.FIRST_NAME_NULL_OR_BLANK ) } if(user.lastName.isNullOrBlank()) { return DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = UserService.ErrorMessages.LAST_NAME_NULL_OR_BLANK ) } if(user.username.isNullOrBlank()) { return DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = UserService.ErrorMessages.USERNAME_NULL_OR_BLANK ) } val savedUser = userRepository.save(user) DatabaseResult( code = ResultCode.CREATION_SUCCESS, entity = savedUser ) } catch(exception: DataAccessException) { DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = exception.message.toString() ) } catch(exception: DataIntegrityViolationException) { DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = exception.message.toString() ) } catch(exception: ConcurrencyFailureException) { DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = exception.message.toString() ) } catch(exception: OptimisticLockingFailureException) { DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = exception.message.toString() ) } catch(exception: Exception) { DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = exception.message.toString() ) } }
测试方法
@Test fun givenExistingUsername_whenSaved_thenReturnFailureResult() { // Given val existingUser = User.Builder() .firstName("John") .lastName("Doe") .username("existing_username") .password("password") .isDeleted(false) .build() userService.saveUser(existingUser) val userWithExistingUsername = User.Builder() .firstName("Jane") .lastName("Doe") .username("existing_username") // Use the same existing username .password("password") .isDeleted(false) .build() // When val savedTest = userService.saveUser(userWithExistingUsername) // Then Mockito.verify(repository, Mockito.never()).save(userWithExistingUsername) // Verify that the save method is never called again assertEquals(ResultCode.CREATION_FAILURE, savedTest.code) assertNull(savedTest.entity) assertNotNull(savedTest.errorMessage) // Adjust the error message verification according to your implementation assertEquals(UserService.ErrorMessages.USERNAME_NOT_UNIQUE, savedTest.errorMessage) }
不管怎么操作,运行测试时第二个相同username的用户总能成功保存,唯一约束像被完全忽略了一样。我是测试新手,需要简单易懂的解释和解决方案。
解决方案
1. 测试误用Mock仓库,未真实触发数据库约束
你用Mockito Mock了userRepository,Mock对象不会真正把数据写入数据库,第一次调用saveUser时数据根本没被持久化,自然不会触发唯一约束。
修复方式:
- 单元测试场景:手动让Mock仓库模拟异常,比如在测试的
When块前添加:Mockito.doThrow(DataIntegrityViolationException::class.java) .`when`(repository) .save(userWithExistingUsername) - 验证真实数据库约束:改用集成测试,使用H2等嵌入式数据库,让Spring Data JPA真正执行数据库操作,才能触发数据库层面的唯一约束。
2. 异常捕获顺序错误
DataIntegrityViolationException是DataAccessException的子类,你先捕获了父类异常,导致子类的DataIntegrityViolationException永远不会被触发,无法返回USERNAME_NOT_UNIQUE的特定错误信息。
修复方式:
把DataIntegrityViolationException的捕获块放到DataAccessException前面:
catch(exception: DataIntegrityViolationException) { DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = UserService.ErrorMessages.USERNAME_NOT_UNIQUE ) } catch(exception: DataAccessException) { DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = exception.message.toString() ) }
3. 测试断言逻辑矛盾
测试里写了Mockito.verify(repository, Mockito.never()).save(userWithExistingUsername),但实际上Service层一定会调用repository.save,异常是在save执行时抛出的,这个断言本身错误。
修复方式:
删除该断言,或改为验证repository.save被调用过一次,同时确认异常被正确捕获并返回失败结果。
4. 可选:提前检查用户名是否存在
如果不想等到数据库抛异常再处理,可在Service层新增前置检查:
if(userRepository.existsByUsername(user.username)) { return DatabaseResult( code = ResultCode.CREATION_FAILURE, errorMessage = UserService.ErrorMessages.USERNAME_NOT_UNIQUE ) }
注意:这种方式存在并发风险(两个请求同时检查同一用户名,都通过后写入数据库),所以数据库层面的唯一约束仍是必须的最后防线。
内容的提问来源于stack exchange,提问作者JamieRhys

