Spring Boot JPA实体REST Docs测试中ID显示null的最优解决方法
问题描述
我用Spring Boot + JPA开发,实体类采用@Builder模式,主键id通过@Id和@GeneratedValue(strategy = GenerationType.IDENTITY)实现自动生成,因此刻意未在Builder使用的构造器中加入id字段。
但编写Spring REST Docs测试时遇到问题:无法在创建实体时设置id值,导致REST Docs输出中id字段显示为null。
相关代码如下:
实体类构造器
@Builder public Webtoon(String title, String summary, String originalImageName, String imagePath, PublishDay publishDay, Platform platform,String author) { this.title = title; this.summary = summary; this.originalImageName = originalImageName; this.imagePath = imagePath; this.publishDay = publishDay; this.platform = platform; this.author = author; }
测试代码
webtoon1 = Webtoon.builder() .title("w1 title") .summary("w1 summary") .originalImageName("w1 imgName") .imagePath("w1 imgPath") .publishDay(PublishDay.THURSDAY) .platform(Platform.NAVER) .author("w1 author") .build(); // webtoon2,webtoon3 @Test public void testWebtoonList() throws Exception { List<WebtoonResponse> mockWebtoonResponses = List.of( new WebtoonResponse(webtoon1), new WebtoonResponse(webtoon2), new WebtoonResponse(webtoon3) ); Mockito.when(webtoonService.findWebtoonList()).thenReturn(mockWebtoonResponses); mockMvc.perform(get(uri)) .andExpect(status().isOk()) .andDo(document("get-v1-get-webtoons", responseFields( fieldWithPath("[].id").description("Webtoon ID"), // 其他字段 ) )); }
我曾考虑在Builder构造器中加入id字段、为测试单独创建含id的构造器、使用Java反射强制设置id,但这些方案都需要修改实体结构,我对此有所顾虑。请问在不破坏实体设计完整性的前提下,如何正确在REST Docs中展示id字段?
附Controller、Service、DTO代码:
Controller
@RequiredArgsConstructor @RequestMapping("/api/v1/webtoon") @RestController public class WebtoonApiController { @GetMapping public ResponseEntity<List<WebtoonResponse>> getWebtoonList() { List<WebtoonResponse> webtoonList = webtoonService.findWebtoonList(); return ResponseEntity.ok(webtoonList); } }
Service
@RequiredArgsConstructor @Service public class WebtoonService { @Transactional(readOnly = true) public List<WebtoonResponse> findWebtoonList() { List<Webtoon> webtoons = webtoonRepository.findAll(); return webtoons.stream() .map(WebtoonResponse::new) .toList(); } }
DTO
@Getter public class WebtoonResponse { private final Long id; private final String title; private final String summary; private final String originalImageName; private final String imagePath; private final PublishDay publishDay; private final Platform platform; private final String author; public WebtoonResponse(Webtoon webtoon) { this.id = webtoon.getId(); this.title = webtoon.getTitle(); this.summary = webtoon.getSummary(); this.originalImageName = webtoon.getOriginalImageName(); this.imagePath = webtoon.getImagePath(); this.publishDay = webtoon.getPublishDay(); this.platform = webtoon.getPlatform(); this.author = webtoon.getAuthor(); } }
解决方案
方案1:直接构造WebtoonResponse(最简洁)
既然测试中是Mock Service的返回值,完全不需要依赖实体类创建DTO。给WebtoonResponse添加全参构造器或@Builder,测试时直接生成带id的DTO实例:
首先给WebtoonResponse补充构造器/Builder:
@Getter @Builder public class WebtoonResponse { private final Long id; private final String title; private final String summary; private final String originalImageName; private final String imagePath; private final PublishDay publishDay; private final Platform platform; private final String author; // 保留原有的实体转换构造器 public WebtoonResponse(Webtoon webtoon) { this.id = webtoon.getId(); this.title = webtoon.getTitle(); this.summary = webtoon.getSummary(); this.originalImageName = webtoon.getOriginalImageName(); this.imagePath = webtoon.getImagePath(); this.publishDay = webtoon.getPublishDay(); this.platform = webtoon.getPlatform(); this.author = webtoon.getAuthor(); } // 全参构造器(使用@Builder可省略手动编写) public WebtoonResponse(Long id, String title, String summary, String originalImageName, String imagePath, PublishDay publishDay, Platform platform, String author) { this.id = id; this.title = title; this.summary = summary; this.originalImageName = originalImageName; this.imagePath = imagePath; this.publishDay = publishDay; this.platform = platform; this.author = author; } }
修改测试代码,直接创建带id的WebtoonResponse:
@Test public void testWebtoonList() throws Exception { List<WebtoonResponse> mockWebtoonResponses = List.of( WebtoonResponse.builder() .id(1L) .title("w1 title") .summary("w1 summary") .originalImageName("w1 imgName") .imagePath("w1 imgPath") .publishDay(PublishDay.THURSDAY) .platform(Platform.NAVER) .author("w1 author") .build(), WebtoonResponse.builder() .id(2L) .title("w2 title") // 填充其他字段 .build() ); Mockito.when(webtoonService.findWebtoonList()).thenReturn(mockWebtoonResponses); mockMvc.perform(get(uri)) .andExpect(status().isOk()) .andDo(document("get-v1-get-webtoons", responseFields( fieldWithPath("[].id").description("Webtoon ID"), // 其他字段 ) )); }
该方案仅修改DTO,对实体类无任何侵入,完全符合设计要求。
方案2:用Spring ReflectionTestUtils设置实体id(不修改业务代码)
Spring提供的ReflectionTestUtils工具类可在测试中通过反射修改对象属性,无需改动实体类代码:
修改测试代码:
@BeforeEach void setUp() { webtoon1 = Webtoon.builder() .title("w1 title") .summary("w1 summary") .originalImageName("w1 imgName") .imagePath("w1 imgPath") .publishDay(PublishDay.THURSDAY) .platform(Platform.NAVER) .author("w1 author") .build(); // 用反射工具设置id ReflectionTestUtils.setField(webtoon1, "id", 1L); webtoon2 = Webtoon.builder()...build(); ReflectionTestUtils.setField(webtoon2, "id", 2L); } @Test public void testWebtoonList() throws Exception { List<WebtoonResponse> mockWebtoonResponses = List.of( new WebtoonResponse(webtoon1), new WebtoonResponse(webtoon2), new WebtoonResponse(webtoon3) ); Mockito.when(webtoonService.findWebtoonList()).thenReturn(mockWebtoonResponses); mockMvc.perform(get(uri)) .andExpect(status().isOk()) .andDo(document("get-v1-get-webtoons", responseFields( fieldWithPath("[].id").description("Webtoon ID"), // 其他字段 ) )); }
此方法完全不改动业务代码,仅在测试环节使用反射工具设置id,对实体设计无破坏。
方案3:Mock Repository而非Service,让JPA生成id
若需贴近真实业务场景,可Mock Repository并借助EntityManager让JPA自动生成id:
@Autowired private EntityManager entityManager; @BeforeEach void setUp() { Webtoon webtoon1 = Webtoon.builder() .title("w1 title") .summary("w1 summary") .originalImageName("w1 imgName") .imagePath("w1 imgPath") .publishDay(PublishDay.THURSDAY) .platform(Platform.NAVER) .author("w1 author") .build(); entityManager.persist(webtoon1); entityManager.flush(); // 此时webtoon1的id已由JPA自动生成 } @Test public void testWebtoonList() throws Exception { // 让Service调用真实Repository(或Mock Repository返回持久化后的实体) List<WebtoonResponse> mockWebtoonResponses = webtoonService.findWebtoonList(); mockMvc.perform(get(uri)) .andExpect(status().isOk()) .andDo(document("get-v1-get-webtoons", responseFields( fieldWithPath("[].id").description("Webtoon ID"), // 其他字段 ) )); }
该方案测试场景更真实,但测试权重稍高,适合需要验证JPA行为的场景。
内容的提问来源于stack exchange,提问作者MINU

