
本文介绍一种基于递归与反射思想的灵活工厂模式,用于在 spring boot 单元测试中按需生成指定类的所有字段组合实例(即动态笛卡尔积),支持任意数量、可选字段,避免硬编码重复样本数据。
本文介绍一种基于递归与反射思想的灵活工厂模式,用于在 spring boot 单元测试中按需生成指定类的所有字段组合实例(即动态笛卡尔积),支持任意数量、可选字段,避免硬编码重复样本数据。
在构建高质量 Spring Boot 应用时,分层测试(如 Service 层、Repository 层)常需大量覆盖不同字段组合的测试数据。若为每个场景手动 new 多个 User 实例,不仅冗余易错,更难以维护和扩展。理想方案是:传入若干字段的候选值列表(如 name: ["Alice","Bob"], role: [[ADMIN], [USER, GUEST]]),自动产出所有合法组合的 User 对象集合——这本质上是运行时的“字段级笛卡尔积”。
✅ 核心设计:状态化工厂类 + 递归笛卡尔展开
我们不依赖外部库(如 Guava 的 Lists.cartesianProduct 仅支持 List>,无法处理字段名映射与缺失字段),而是构建一个类型感知的
UserFactory:
public class UserFactory {
private String name;
private String email;
private int age;
private Role[] role;
// 支持链式调用的 setter(提升可读性)
public UserFactory name(String name) { this.name = name; return this; }
public UserFactory email(String email) { this.email = email; return this; }
public UserFactory age(int age) { this.age = age; return this; }
public UserFactory role(Role[] role) { this.role = role; return this; }
public User createUser() {
User user = new User();
user.setName(this.name);
user.setEmail(this.email);
user.setAge(this.age);
user.setRole(this.role != null ? this.role.clone() : null); // 防止数组共享
return user;
}
// 动态设值(按字段名 + 类型安全转换)
public void setPropertyByName(String name, Object value) {
switch (name.toLowerCase()) {
case "name": this.name = (String) value; break;
case "email": this.email = (String) value; break;
case "age": this.age = ((Number) value).intValue(); break;
case "role": this.role = (Role[]) value; break;
default: throw new IllegalArgumentException("Unknown property: " + name);
}
}
// ? 主力方法:递归生成笛卡尔积对象列表
public static List<User> createUsers(Map<String, List<?>> properties, UserFactory baseFactory) {
// 过滤空列表,取首个非空字段作为当前递归维度
Optional<Map.Entry<String, List<?>>> first = properties.entrySet().stream()
.filter(e -> !e.getValue().isEmpty())
.findFirst();
if (first.isEmpty()) {
return List.of(baseFactory.createUser()); // 终止:无更多字段,生成单个实例
}
Map<String, List<?>> remaining = new HashMap<>(properties);
String currentProp = first.get().getKey();
remaining.remove(currentProp);
return first.get().getValue().stream()
.flatMap(value -> {
UserFactory forked = new UserFactory()
.name(baseFactory.name)
.email(baseFactory.email)
.age(baseFactory.age)
.role(baseFactory.role);
forked.setPropertyByName(currentProp, value);
return createUsers(remaining, forked).stream();
})
.toList();
}
}? 使用示例:灵活组合,零空指针风险
@Test
void testUserCartesianGeneration() {
// 场景1:提供 name、age、role,email 缺失 → 全为 null
Map<String, List<?>> props1 = Map.of(
"name", Arrays.asList("Alice", "Bob"),
"age", Arrays.asList(25, 30),
"role", Arrays.asList(new Role[]{Role.ADMIN}, new Role[]{Role.USER, Role.GUEST})
);
List<User> users1 = UserFactory.createUsers(props1, new UserFactory());
assertEquals(2 * 2 * 2, users1.size()); // 8 个组合
// 场景2:仅 name 和 role,age 固定为 28(通过 baseFactory 预设)
UserFactory fixedAgeFactory = new UserFactory().age(28);
Map<String, List<?>> props2 = Map.of(
"name", Arrays.asList("Charlie"),
"role", Arrays.asList(new Role[]{Role.ADMIN})
);
List<User> users2 = UserFactory.createUsers(props2, fixedAgeFactory);
assertEquals(1, users2.size());
assertEquals(28, users2.get(0).getAge()); // age 来自 factory,非笛卡尔维度
}⚠️ 关键注意事项
-
数组/集合深拷贝:
role是引用类型数组,createUser()中必须.clone()或Arrays.copyOf(),否则所有User实例将共享同一数组,修改一个会影响全部。 -
枚举/不可变对象安全:
Role若为enum则无需拷贝;但List<role></role>等需显式复制。 -
性能提示:笛卡尔积结果数 = 各字段列表长度乘积。若某字段有 100 个值,其他字段各 10 个,则生成 10,000 个对象——请结合
@TestInstance(TestInstance.Lifecycle.PER_CLASS)或@ParameterizedTest分批验证。 -
扩展性:该模式可轻松泛化为
GenericFactory<t></t>(配合Class<t></t>和Field反射),但对测试场景而言,为每个 DTO 定制工厂更清晰、类型安全、调试友好。
✅ 总结:此方案不引入第三方依赖,完全兼容 Spring Boot 测试生态;通过“预设默认值 + 动态笛卡尔维度”双机制,优雅解决字段可选性问题;代码结构清晰,易于单元测试覆盖,是构建高覆盖率测试数据的理想基础设施。

















