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); }
问题原因
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),断言失败。
- 第一次调用
查询结果顺序无保证
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
相关产品推荐
相关产品推荐

