Spring存储库并不总是抛出DataIntegrityViolationException

时间:2017-04-30 14:57:25

标签: java spring junit spring-data spring-restcontroller

我正在使用Spring制作REST API,但我无法对其进行单元测试。 我写了一个端点来更新用户组,当我在我的前端创建一个具有重复名称的组时,它确实发送了409冲突(unique = true)。但是,当我进行单元测试时,它没有。我发现在单元测试结束时添加这一行groupRepository.findAll().forEach(g -> System.out.println(g.getName()));确实会抛出409。

端点:

@Override
public ResponseEntity<Object> update(@ApiParam(value = "form object to add to the store", required = true) @Valid @RequestBody FormGroupDTO group, @ApiParam(value = "Id of the form that needs to be updated", required = true) @PathVariable("groupId") Long groupId, @ApiParam(value = "token to be passed as a header", required = true) @RequestHeader(value = "token", required = true) String token) {
    String name = JWTutils.getEmailInToken(token);

    if(name == null) {
        return new ResponseEntity<>(HttpStatus.FORBIDDEN);
    }

    User user = userRepository.findByEmail(name);

    if(user == null){
        return  new ResponseEntity<>(HttpStatus.FORBIDDEN);
    }

    FormGroup groupModel = groupRepository.findByIdAndAdmin(groupId, user);

    if(groupModel == null){
        return  new ResponseEntity<>(HttpStatus.FORBIDDEN);
    }

    if(group.getMembers().stream().filter(m -> m.getRole() == UserFormGroupRole.ADMIN).toArray().length == 0){
        return new ResponseEntity<>(new ValidationErrorDTO("noAdmin", "MEMBER.NOADMIN"), HttpStatus.BAD_REQUEST);
    }

    // Get users
    groupModel.getUserFormGroups().clear();
    for(MemberDTO member : group.getMembers()){
        User u = userRepository.findByEmail(member.getEmail());
        if(u == null){
            return new ResponseEntity<>(new ValidationErrorDTO("notexist", "ADDUSER.NOTEXIST"), HttpStatus.BAD_REQUEST);
        }
        if(u.getRole() == UserRole.USER){
            return new ResponseEntity<>(new ValidationErrorDTO("notexist", "ADDUSER.NOTPRIVILEGED"), HttpStatus.BAD_REQUEST);
        }
        UserFormGroup ufg = userFormGroupRepository.findByUserAndFormGroup(u, groupModel);
        if(ufg == null){
            groupModel.getUserFormGroups().add(new UserFormGroup(u, groupModel, member.getRole()));
        } else{
            ufg.setRole(member.getRole());
            groupModel.getUserFormGroups().add(ufg);
        }
    }

    groupModel.setName(group.getName());

    try{
        groupRepository.save(groupModel);
        return  new ResponseEntity<>(HttpStatus.NO_CONTENT);
    } catch (DataIntegrityViolationException e){
        System.out.println(e.getMessage());
        System.out.println(e.getClass());
        return  new ResponseEntity<>(HttpStatus.CONFLICT);
    }
}

我的单元测试:

@Test
public void updateGroupNameAlreadyInUse() throws Exception {
    groupRepository.save(new FormGroup("newFormGroup", user));
    this.mockMvc.perform(put("/groups/" + group.getId())
            .header("token", token)
            .content(json(new FormGroupDTO("newFormGroup", group.getUserFormGroups().stream().map(ufg -> new MemberDTO(ufg.getUser().getEmail(), ufg.getRole())).collect(Collectors.toList()))))
            .contentType(contentType))
            .andExpect(status().isConflict());
}

我的CrudRepository的save函数并不总是抛出DataIntegrityViolationException。我刚刚意识到,我的单元测试groupRepository.save(new FormGroup("newFormGroup", user));的第一行可能在单元测试结束之前没有执行,findAll函数会触发它。

1 个答案:

答案 0 :(得分:1)

简短的故事:在测试中插入后,您需要手动执行flush()。现在让我们详细讨论。我假设您正在使用ID生成策略,如Sequence或UUID或类似的东西。

有许多事情需要考虑:

冲洗

  • FlushMode - 确定ORM何时触发SQL语句。默认情况下,它会在任何SELECT语句之前和事务提交之前触发。获取每条记录名称的解决方案会发出SELECT语句 - 这会触发刷新所有挂起的SQL语句。
  • save()persist()保证返回持久对象。这样的对象必须有ID。某些ID生成策略(如Identity)需要INSERT语句来生成ID。其他人(如SequenceUUID) - 不要。因此,ORM可以在不插入记录的情况下获取ID(它希望尽可能地延迟某些优化)。

因此,在进行与ORM相关的测试时,您必须先手动调用flush(),然后再对该数据执行任何操作。

交易&amp;会话

当您将事物标记为@Transactional时,行为是:

  1. 查看此线程中是否已打开事务。
    • 如果是 - 不要做任何事。
    • 如果不是 - 创建一个事务并将其绑定到当前线程(通过ThreadLocal变量)。
  2. 方法完成后 - 检查交易是否由我启动。
    • 如果没有 - 什么也不做。
    • 如果是 - 提交交易,关闭会话。
  3. 我假设您使用@Transactional标记了您的测试。这意味着它测试谁开始会话和交易。存储库只使用已经打开的存储库。然后因为没有提交 - 没有刷新。然后你使用MockMvc - 在同一个线程中工作。它经过@TransactionalOSIV,也发现该事务已经启动。因此,交易被重复使用。

    然后它转到你的核心逻辑 - 你正在做一些SELECT语句,这会刷新当前会话中的挂起的SQL语句。所以你原来的save()只是脸红了。

    现在,在您的逻辑结束时,您再次执行save(),将INSERT语句放入挂起的SQL查询中。测试完成后,它只会回滚交易,最终INSERT不会发生。除非..你正在做你提到的SELECT陈述。

    最后 - 不要忘记在ORM相关测试中执行flush()clear()。这些方法存在于Hibernate的Session或JPA&#39; EntityManager中。前者可以通过以下方式完成:SessionFactory#getCurrentSession()。后者可以注入:

    @PersistenceContext 
    EntityManager entityManager;
    

    PS:我没有看到任何标有@Transactional的生产代码。如果你不这样做,你可能会遇到问题。

    PPS:this is not a unit test