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

Java Record如何高效创建带默认值的测试夹具?

问题

需要对接收Java Record类作为入参的方法做单元测试,该Record字段多,方法业务逻辑复杂。测试时要创建大量测试Record实例,每个实例仅在默认Record基础上修改少量字段。但Java Record不可变,没有setter,Java也不支持方法参数默认值,试过以下方案都有问题:

  • Telescoping fixtures:样板代码多,同类型字段易出错;
  • Mockito模拟getter:无法模拟final类;
  • 复制粘贴:代码冗余难维护;
  • Lombok Builder:高效但不够优雅。

示例代码:

record Customer(
  String name,
  int ageInYears,
  boolean isPremiumCustomer,
  boolean isRegisteredInApp,
  int totalNumberOfPurchases,
  int totalPurchaseValue,
  int numberOfPurchasesLastMonth,
  int purchaseValueLastMonth
) {}

class DiscountCalculator {
  /**
   * Calculates discount for given customer in percent.
   */
  static int calculateDiscountFor(Customer customer) {
    // 复杂业务逻辑实现
  }
}

测试策略示例:

class DiscountCalculatorTest {
  private static final Customer DEFAULT_CUSTOMER = new Customer("Jane Doe", 45, false, false, 10, 100, 1, 10);

  @Test
  void defaultCase() {
    // given
    var customer = DEFAULT_CUSTOMER;

    // when
    var result = DiscountCalculator.calculateDiscountFor(customer);

    // then
    assertThat(result).isEqualTo(0);
  }
}
解决方案

1. 给Record添加withXxx风格的修改方法

利用Record的不可变性,手动为每个字段添加返回新实例的with方法,基于默认实例链式修改需要的字段,代码简洁且可读性强。

修改后的Customer Record:

record Customer(
  String name,
  int ageInYears,
  boolean isPremiumCustomer,
  boolean isRegisteredInApp,
  int totalNumberOfPurchases,
  int totalPurchaseValue,
  int numberOfPurchasesLastMonth,
  int purchaseValueLastMonth
) {
  public Customer withName(String name) {
    return new Customer(name, this.ageInYears, this.isPremiumCustomer, this.isRegisteredInApp,
                        this.totalNumberOfPurchases, this.totalPurchaseValue,
                        this.numberOfPurchasesLastMonth, this.purchaseValueLastMonth);
  }

  public Customer withAgeInYears(int ageInYears) {
    return new Customer(this.name, ageInYears, this.isPremiumCustomer, this.isRegisteredInApp,
                        this.totalNumberOfPurchases, this.totalPurchaseValue,
                        this.numberOfPurchasesLastMonth, this.purchaseValueLastMonth);
  }

  // 其他字段的with方法以此类推
}

测试中使用方式:

@Test
void premiumCustomerGets10PercentDiscount() {
  // given
  var customer = DEFAULT_CUSTOMER.withIsPremiumCustomer(true);

  // when
  var result = DiscountCalculator.calculateDiscountFor(customer);

  // then
  assertThat(result).isEqualTo(10);
}

@Test
void highVolumeCustomerGets5PercentDiscount() {
  // given
  var customer = DEFAULT_CUSTOMER.withTotalNumberOfPurchases(50)
                                 .withTotalPurchaseValue(1000);

  // when
  var result = DiscountCalculator.calculateDiscountFor(customer);

  // then
  assertThat(result).isEqualTo(5);
}

这种方式完全符合Record的设计初衷,代码直观,无额外依赖,维护成本低。

2. 手动实现测试专用Builder

如果不想修改原Record类,可在测试类中实现仅用于测试的Builder,基于默认实例进行字段覆盖。

测试类中的Builder实现:

class DiscountCalculatorTest {
  private static final Customer DEFAULT_CUSTOMER = new Customer("Jane Doe", 45, false, false, 10, 100, 1, 10);

  private static class CustomerBuilder {
    private String name = DEFAULT_CUSTOMER.name();
    private int ageInYears = DEFAULT_CUSTOMER.ageInYears();
    private boolean isPremiumCustomer = DEFAULT_CUSTOMER.isPremiumCustomer();
    private boolean isRegisteredInApp = DEFAULT_CUSTOMER.isRegisteredInApp();
    private int totalNumberOfPurchases = DEFAULT_CUSTOMER.totalNumberOfPurchases();
    private int totalPurchaseValue = DEFAULT_CUSTOMER.totalPurchaseValue();
    private int numberOfPurchasesLastMonth = DEFAULT_CUSTOMER.numberOfPurchasesLastMonth();
    private int purchaseValueLastMonth = DEFAULT_CUSTOMER.purchaseValueLastMonth();

    private CustomerBuilder() {}

    public static CustomerBuilder aCustomer() {
      return new CustomerBuilder();
    }

    public CustomerBuilder withName(String name) {
      this.name = name;
      return this;
    }

    public CustomerBuilder withIsPremiumCustomer(boolean isPremiumCustomer) {
      this.isPremiumCustomer = isPremiumCustomer;
      return this;
    }

    // 其他字段的setter方法以此类推

    public Customer build() {
      return new Customer(name, ageInYears, isPremiumCustomer, isRegisteredInApp,
                          totalNumberOfPurchases, totalPurchaseValue,
                          numberOfPurchasesLastMonth, purchaseValueLastMonth);
    }
  }

  @Test
  void registeredAppUserGets3PercentDiscount() {
    // given
    var customer = CustomerBuilder.aCustomer()
                                  .withIsRegisteredInApp(true)
                                  .build();

    // when
    var result = DiscountCalculator.calculateDiscountFor(customer);

    // then
    assertThat(result).isEqualTo(3);
  }
}

这种方式不侵入原业务代码,Builder完全服务于测试场景,默认值继承自DEFAULT_CUSTOMER,修改字段时只需调用对应方法即可。

3. 使用Java 17+的record pattern结合解构

如果使用Java 17及以上,可利用record pattern解构默认实例的字段,再替换需要修改的部分,代码更简洁:

@Test
void recentHighSpenderGets8PercentDiscount() {
  // given
  var (name, age, premium, registered, totalPurchases, totalValue, _, _) = DEFAULT_CUSTOMER;
  var customer = new Customer(name, age, premium, registered, totalPurchases, totalValue, 5, 200);

  // when
  var result = DiscountCalculator.calculateDiscountFor(customer);

  // then
  assertThat(result).isEqualTo(8);
}

这种方式无需额外写方法,适合快速修改少量字段的场景,但如果修改的字段位置靠后,需要写出前面的所有字段占位符,更适合字段数量不多或修改位置靠前的情况。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 23:14:56