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

Java一对多关联单元测试断言失败问题排查求助

单元测试断言失败问题排查

测试代码

@Test
public void testFindPetsByOwner() {
    CustomerDTO customerDTO = createCustomerDTO();
    CustomerDTO newCustomer = userController.saveCustomer(customerDTO);

    PetDTO petDTO = createPetDTO();
    petDTO.setOwnerId(newCustomer.getId());
    PetDTO newPet = petController.savePet(petDTO);
    petDTO.setType(PetType.DOG);
    petDTO.setName("DogName");
    PetDTO newPet2 = petController.savePet(petDTO);

    List<PetDTO> pets = petController.getPetsByOwner(newCustomer.getId());
    Assertions.assertEquals(pets.size(), 2);
    Assertions.assertEquals(pets.get(0).getOwnerId(), newCustomer.getId());
    Assertions.assertEquals(pets.get(0).getId(), newPet.getId());
}

报错信息

org.opentest4j.AssertionFailedError: 
Expected :1
Actual   :2
<Click to see difference>

相关类代码

PetController.java

@PostMapping
public PetDTO savePet(@RequestBody PetDTO petDTO) {
    Pet newPet = new Pet();
    BeanUtils.copyProperties(petDTO, newPet, "id");
    Pet savedPet = petService.save(newPet);
    BeanUtils.copyProperties(savedPet, petDTO);
    return petDTO;
}

@GetMapping("/owner/{ownerId}")
public List<PetDTO> getPetsByOwner(@PathVariable long ownerId) {
    List<Pet> petsByOwner = petService.getPetsByOwner(ownerId);
    List<PetDTO> petDTOList = new ArrayList<>();
    for(Pet pet:petsByOwner){
        PetDTO petDTO = new PetDTO();
        BeanUtils.copyProperties(pet, petDTO);
        petDTOList.add(petDTO);
    }
    return petDTOList;
}

PetService.java

public Pet save(Pet pet){
    Customer customer = customerRepository.findById(pet.getOwnerId()).orElseThrow(() -> new CustomerNotFoundException());
    pet.setCustomer(customer);
    customer.getPets().add(pet);
    customerRepository.save(customer);
    List<Pet> savedPets =  customer.getPets();
    Pet nPet =  savedPets.get(savedPets.size() - 1 );
    return nPet;
}

public List<Pet> getPetsByOwner(long ownerId) {
    return petRepository.findByOwnerId(ownerId);
}

PetDTO.java

public class PetDTO {
    private long id;
    private PetType type;
    private String name;
    private long ownerId;
    private LocalDate birthDate;
    private String notes;
    // getters and setters
}

Pet.java

@Entity
public class Pet {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    @NotNull
    @Enumerated(EnumType.STRING)
    private PetType type;

    private String name;
    
    private long ownerId;

    private LocalDate birthDate;

    private String notes;

    @ManyToMany(mappedBy = "pets")
    private List<Schedule> schedules;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    //getters and setters
}

Customer.java

@Entity
public class Customer extends Person {
    private String phoneNumber;
    private String notes;
    
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "customer", cascade = CascadeType.ALL )
    private List<Pet> pets = new ArrayList<>();
    //getters and setters
}

Person.java

@MappedSuperclass
public class Person {
    @Id
    // watch out if this generation strategy becomes a bug later
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected long id;
    @Nationalized
    private String name;
    // ...其他属性和方法
}

PetRepository.java

@Repository
public interface PetRepository extends JpaRepository<Pet, Long> {
    List<Pet> findByOwnerId(long ownerId);
}

问题原因

  1. DTO引用复用导致属性覆盖
    测试中复用了同一个petDTO实例,而PetController.savePet方法直接修改传入的petDTO并返回它。这意味着newPet与petDTO指向同一个对象:

    • 第一次调用savePet后,petDTO的id被赋值为第一个宠物的ID,newPet的id同步更新;
    • 第二次调用savePet时,方法会将第二个宠物的ID赋值给petDTO,导致newPet的id被覆盖为第二个宠物的ID;
    • 最终断言时,newPet.getId()是第二个宠物的ID(2),而pets.get(0)是数据库中第一个宠物的ID(1),断言失败。
  2. 查询结果顺序无保证
    petRepository.findByOwnerId(ownerId)返回的列表顺序未通过排序规则明确指定,pets.get(0)可能是任意一个宠物,不一定是第一个保存的实例,进一步增加断言失败的概率。

解决方案

方案1:测试中避免复用DTO实例

每次保存宠物时创建新的PetDTO对象,避免引用复用:

@Test
public void testFindPetsByOwner() {
    CustomerDTO customerDTO = createCustomerDTO();
    CustomerDTO newCustomer = userController.saveCustomer(customerDTO);

    // 第一个宠物使用独立DTO
    PetDTO petDTO1 = createPetDTO();
    petDTO1.setOwnerId(newCustomer.getId());
    PetDTO newPet = petController.savePet(petDTO1);

    // 第二个宠物使用新的DTO
    PetDTO petDTO2 = createPetDTO();
    petDTO2.setOwnerId(newCustomer.getId());
    petDTO2.setType(PetType.DOG);
    petDTO2.setName("DogName");
    PetDTO newPet2 = petController.savePet(petDTO2);

    List<PetDTO> pets = petController.getPetsByOwner(newCustomer.getId());
    Assertions.assertEquals(pets.size(), 2);
    // 验证两个宠物ID都存在于结果中,不依赖顺序
    Assertions.assertTrue(pets.stream().anyMatch(p -> p.getId() == newPet.getId()));
    Assertions.assertTrue(pets.stream().anyMatch(p -> p.getId() == newPet2.getId()));
}

方案2:修改Controller返回新的DTO实例

调整savePet方法,创建并返回新的PetDTO对象,避免修改传入的参数:

@PostMapping
public PetDTO savePet(@RequestBody PetDTO petDTO) {
    Pet newPet = new Pet();
    BeanUtils.copyProperties(petDTO, newPet, "id");
    Pet savedPet = petService.save(newPet);
    // 创建新的DTO实例,避免修改传入的参数
    PetDTO resultDTO = new PetDTO();
    BeanUtils.copyProperties(savedPet, resultDTO);
    return resultDTO;
}

方案3:断言时不依赖列表顺序

通过流操作验证目标ID是否存在于结果列表中,避免依赖不确定的排序:

Assertions.assertTrue(pets.stream().anyMatch(p -> p.getId().equals(newPet.getId())));
Assertions.assertTrue(pets.stream().anyMatch(p -> p.getId().equals(newPet2.getId())));

内容的提问来源于stack exchange,提问作者Othello

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 03:54:56