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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 21:41:03