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

REST Assured测试多对一关联实体时Company属性为空问题排查

多对一关联实体REST Assured测试中Company属性接收为null的问题

问题描述

我有一个包含多对一关联的实体,通过Postman手动验证接口功能正常,但使用REST Assured编写单元测试时,尽管在setup中设置了UserEntity的Company属性,API接收请求时该属性却为null。调试显示发送请求前User对象的Company属性已设置,但请求到达POST端点时变为null。

User类

@Getter
@Setter
@Entity(name = "User")
@Table(name = "users")
@NamedQueries(
        @NamedQuery(name = CustomNamedQueries.USER_GET_ALL_IN_COMPANY_DEFINITION, query = CustomNamedQueries.USER_GET_ALL_IN_COMPANY_QUERY)
)
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;

    @ManyToOne
    @JsonBackReference
    @JoinColumn(name = "company_id", referencedColumnName = "id")
    private CompanyEntity company;
}

Company类

@Getter
@Setter
@Entity(name = "Company")
@Table(name = "companies")
public class CompanyEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;

    @Email
    @NotEmpty(message = "A company email is required.")
    @Column(name = "company_email")
    private String companyEmail;

    @JsonManagedReference
    @OneToMany(mappedBy = "company", fetch = FetchType.EAGER)
    private List<UserEntity> users;
}

测试类

@QuarkusTransactionalTest
public class UserControllerIntegrationTest {

    @Inject
    CompanyRepository companyRepo;

    @Inject
    UserRepository userRepo;

    private CompanyEntity company;

    @BeforeEach
    void setup() {
        company = saveCompany();
    }

    @AfterEach
    void cleanup() {
        userRepo.deleteAll();
        companyRepo.deleteAll();
        company = null;
    }

    @Test
    void test_getUsersInCompany() {
        // Given
        UserEntity user1 = saveFromAPI();
        UserEntity user2 = saveFromAPI();

        // When
        List<UserEntity> users = getUsersInACompany();

        // Then
        assertEquals(2, users.size());
        assertThat(users.get(0)).isEqualTo(user1);
        assertThat(users.get(1)).isEqualTo(user2);
    }

    private UserEntity saveFromAPI() {
        UserEntity user = createAdminUser(company);
        return given()
                .contentType(ContentType.JSON)
                .body(user)
                    .post("/api/v1/companies/{companyId}/users", company.getId())
                .then()
                    .statusCode(201)
                    .extract()
                        .as(UserEntity.class);
    }

    public static UserEntity createUser(CompanyEntity company, boolean isAdmin) {
        LocalDateTime localDateTime = LocalDateTime.now();
        UserEntity user = new UserEntity();
        user.setAddress(createAddress());
        user.setCompany(company);
        user.setDateOfBirth(DateTimeFormatter.ofPattern("dd-MM-yyyy", Locale.ENGLISH).format(localDateTime));
        user.setEmploymentStartDate(DateTimeFormatter.ofPattern("dd-MM-yyyy", Locale.ENGLISH).format(localDateTime));
        user.setEmail(RandomStringUtils.randomAlphabetic(6) + "@mailer.com");
        user.setTitle(RandomStringUtils.randomAlphabetic(4));
        user.setEmployeeNumber(RandomStringUtils.randomNumeric(5));
        user.setJobTitle(RandomStringUtils.randomAlphabetic(8));
        user.setStatus(RandomStringUtils.randomAlphabetic(7));
        user.setQualification(RandomStringUtils.randomAlphabetic(10));
        user.setMobile(RandomStringUtils.randomNumeric(10));
        user.setFirstName(RandomStringUtils.randomAlphabetic(10));
        user.setLastName(RandomStringUtils.randomAlphabetic(10));
        user.setIsAdmin(isAdmin);
        return user;
    }
}

Postman有效请求体

{
    "company": {
        "id": 1
    },
    "address": {
        "number": "14",
        "street": "Harcourt Street",
        "city": "Dublin 5",
        "county": "Dublin",
        "country": "Ireland",
        "postcode": "R34 X943"
    },
    "title": "Mr",
    "firstName": "Bilbo",
    "lastName": "Baggins",
    "employeeNumber": "1B345",
    "jobTitle": "Manager",
    "status": "Active",
    "employmentStartDate": "10-04-2000",
    "dateOfBirth": "21-09-1980",
    "qualification": "IT Manager",
    "mobile": "0987654321",
    "email": "bilbo@baggins.com",
    "isAdmin": true
}

问题原因

核心问题出在@JsonBackReference注解:

  • 这个注解的作用是阻止Jackson序列化关联对象,避免Company和User之间的循环引用问题。当测试中把UserEntity对象传入REST Assured的.body(user)时,Jackson序列化UserEntity时会直接忽略company属性,导致请求体里完全没有company字段,后端自然接收不到。
  • Postman是手动构造的JSON,明确包含了{"company": {"id": 1}},所以后端能正确解析关联关系。

解决方案

方案1:调整序列化注解,保留company字段序列化

把UserEntity中的@JsonBackReference替换为@JsonIgnoreProperties,只忽略Company中的users属性来避免循环引用,同时保留company本身的序列化:

@ManyToOne
@JsonIgnoreProperties("users")
@JoinColumn(name = "company_id", referencedColumnName = "id")
private CompanyEntity company;

方案2:测试时手动构造JSON请求体

不直接传入UserEntity对象,而是手动构造符合Postman格式的JSON数据,确保包含company.id字段:

private UserEntity saveFromAPI() {
    // 构造请求用的JSON结构
    Map<String, Object> userJson = new HashMap<>();
    userJson.put("address", createAddress());
    
    // 只传入company的id
    Map<String, Object> companyJson = new HashMap<>();
    companyJson.put("id", company.getId());
    userJson.put("company", companyJson);
    
    // 填充其他用户字段
    LocalDateTime localDateTime = LocalDateTime.now();
    userJson.put("dateOfBirth", DateTimeFormatter.ofPattern("dd-MM-yyyy", Locale.ENGLISH).format(localDateTime));
    userJson.put("employmentStartDate", DateTimeFormatter.ofPattern("dd-MM-yyyy", Locale.ENGLISH).format(localDateTime));
    userJson.put("email", RandomStringUtils.randomAlphabetic(6) + "@mailer.com");
    userJson.put("title", RandomStringUtils.randomAlphabetic(4));
    userJson.put("employeeNumber", RandomStringUtils.randomNumeric(5));
    userJson.put("jobTitle", RandomStringUtils.randomAlphabetic(8));
    userJson.put("status", RandomStringUtils.randomAlphabetic(7));
    userJson.put("qualification", RandomStringUtils.randomAlphabetic(10));
    userJson.put("mobile", RandomStringUtils.randomNumeric(10));
    userJson.put("firstName", RandomStringUtils.randomAlphabetic(10));
    userJson.put("lastName", RandomStringUtils.randomAlphabetic(10));
    userJson.put("isAdmin", true);

    return given()
            .contentType(ContentType.JSON)
            .body(userJson)
                .post("/api/v1/companies/{companyId}/users", company.getId())
            .then()
                .statusCode(201)
                .extract()
                    .as(UserEntity.class);
}

方案3:使用DTO(数据传输对象)—— 推荐

这是最规范的架构设计,避免直接用实体类作为请求/响应载体:

  1. 创建UserCreateDTO,只包含前端需要传递的字段:
@Getter
@Setter
public class UserCreateDTO {
    private AddressDTO address;
    private Long companyId; // 仅传递公司ID即可
    // 其他用户字段:title、firstName、lastName等...
}
  1. 控制器接收DTO,转换为实体并手动关联Company:
@PostMapping("/api/v1/companies/{companyId}/users")
public ResponseEntity<UserEntity> createUser(@PathVariable Long companyId, @RequestBody UserCreateDTO dto) {
    CompanyEntity company = companyRepo.findById(companyId)
            .orElseThrow(() -> new RuntimeException("Company not found"));
    UserEntity user = new UserEntity();
    // 复制DTO字段到实体
    user.setAddress(dto.getAddress());
    user.setCompany(company);
    user.setFirstName(dto.getFirstName());
    // ...其他字段赋值
    userRepo.save(user);
    return ResponseEntity.status(201).body(user);
}
  1. 测试时构造UserCreateDTO传入,既解决序列化问题,又符合分层架构的职责分离原则。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 19:03:30