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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 08:50:00