如何无重复代码生成Java不可变类的测试数据实例?
针对Lombok @Value不可变类的单元测试对象生成优化方案
问题描述
我有一个用Lombok的@Value注解标记的大型不可变类,需要对它的验证逻辑做单元测试。测试时要生成多个和有效实例仅个别属性不同的对象,但不想每次都重复写完整的对象构造代码。如果是可变类,我只要生成一个有效实例再修改对应属性就行,但不可变类只能每次全量构造,这种写法太冗余。试过创建结构相同的可变类再转不可变对象,但这个方案不太理想,有没有更合理的参数化实现方式?
示例代码
@Value class MyClass{ String a; String b; String c; ... } // 可变类的理想写法(不可用): getValid() { return new MyClass("a","b","c"...); } testA() { var obj = getValid(); obj.a = "invalid"; // ... 执行测试 } // 不可变类当前冗余写法: testA() { var obj = new MyClass("invalid","b","c"...); // 希望避免这种写法 // ... 执行测试 }
解决方案
1. 利用Lombok @With注解生成属性拷贝方法
Lombok的@With注解会为每个属性生成withXxx()方法,调用后返回新的不可变实例,仅修改指定属性,其余属性与原对象一致,完全契合不可变对象的设计原则。
修改原类
@Value @With // 添加该注解 class MyClass{ String a; String b; String c; // ... 其他属性 }
测试代码优化
// 定义全局有效实例生成方法 private MyClass getValidInstance() { return new MyClass("a", "b", "c"...); } @Test void testInvalidA() { MyClass invalidObj = getValidInstance().withA("invalid"); // 执行验证逻辑测试,例如断言抛出ValidationException } @Test void testInvalidB() { MyClass invalidObj = getValidInstance().withB("invalid"); // 执行测试 }
2. 使用Lombok @Builder配合toBuilder功能
给@Value类添加@Builder(toBuilder = true)注解,可从已有实例生成Builder,修改指定属性后构建新实例,适合需要同时修改多个属性的场景。
修改原类
@Value @Builder(toBuilder = true) // 开启toBuilder功能 class MyClass{ String a; String b; String c; // ... 其他属性 }
测试代码优化
private MyClass getValidInstance() { return MyClass.builder().a("a").b("b").c("c")...build(); } @Test void testInvalidA() { MyClass invalidObj = getValidInstance().toBuilder().a("invalid").build(); // 执行测试 } @Test void testInvalidBAndC() { // 同时修改多个属性的场景 MyClass invalidObj = getValidInstance().toBuilder() .b("invalidB") .c("invalidC") .build(); // 执行测试 }
3. 自定义测试数据构建器+JUnit参数化测试
如果需要批量测试多种无效场景,可结合JUnit 5参数化测试和自定义构建器,减少重复代码并统一管理测试数据。
自定义测试构建器
class MyClassTestBuilder { // 默认赋值为有效属性值 private String a = "a"; private String b = "b"; private String c = "c"; // ... 其他属性默认值 // 定义各种无效场景的方法 public MyClassTestBuilder withInvalidA() { this.a = "invalid"; return this; } public MyClassTestBuilder withNullB() { this.b = null; return this; } public MyClassTestBuilder withEmptyC() { this.c = ""; return this; } // 构建最终的MyClass实例 public MyClass build() { return new MyClass(a, b, c...); } }
参数化测试实现
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import java.util.stream.Stream; class MyClassValidationTest { // 提供测试场景:每个元素对应一种无效构建方式 static Stream<MyClass> invalidInstances() { return Stream.of( new MyClassTestBuilder().withInvalidA().build(), new MyClassTestBuilder().withNullB().build(), new MyClassTestBuilder().withEmptyC().build() ); } @ParameterizedTest @MethodSource("invalidInstances") void testValidation(MyClass invalidObj) { // 执行统一的验证逻辑测试,例如断言验证失败 assertThrows(ValidationException.class, () -> validator.validate(invalidObj)); } }
内容的提问来源于stack exchange,提问作者maybesomename
相关产品推荐
相关产品推荐

