如何可靠单元测试Java拷贝构造函数的全属性深克隆?
问题描述
我项目里有一批简单的模型类,示例如下:
public class Car { public String model = ""; public Instant productionDate = null; public List<Tire> tires = new ArrayList<>(); // .... } public class Tire { public String maker = ""; public int size = 0; // ... } // ...
我打算通过拷贝构造函数实现深克隆,示例代码如下:
public class Car { public Car(Car original) { this.model = original.model; this.productionDate = original.productionDate; this.tires.addAll(original.tires.stream().map(tire -> new Tire(tire)).toList()); } }
常规单元测试方式是创建对象、调用拷贝构造函数生成克隆对象,再断言两者属性一致。但这种方式存在隐患——如果有人给类新增字段,很可能忘记同步更新拷贝构造函数和测试断言。有没有更优的单元测试方式?比如无需显式指定属性,就能自动比较原对象与克隆对象的所有属性?
可行解决方案
1. 用断言库自动对比全属性
直接借助成熟的断言库实现对象全属性递归对比,无需手动编写每个字段的断言:
- AssertJ:可以使用
usingRecursiveComparison()强制递归对比所有字段(即使类没重写equals方法):
import org.assertj.core.api.Assertions; // 测试代码示例 Car original = new Car(); // 为original的所有字段赋值(包括嵌套的tires集合) original.model = "Model X"; original.productionDate = Instant.now(); original.tires.add(new Tire("Michelin", 18)); Car cloned = new Car(original); // 递归对比原对象与克隆对象的所有属性 Assertions.assertThat(cloned).usingRecursiveComparison().isEqualTo(original);
- Hamcrest:通过
samePropertyValuesAs匹配器实现全属性对比:
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs; // 测试代码示例 assertThat(cloned, samePropertyValuesAs(original));
2. 自定义反射工具类实现深对比
如果不想引入第三方库,可以自己基于反射实现一个深对比工具方法,遍历对象的所有字段(包括私有字段、嵌套对象、集合、数组)进行逐一对比:
import java.lang.reflect.Field; import java.util.Collection; import java.util.Iterator; import java.util.Objects; public class ObjectComparator { public static boolean deepEquals(Object obj1, Object obj2) throws IllegalAccessException { if (obj1 == obj2) return true; if (obj1 == null || obj2 == null) return false; Class<?> clazz = obj1.getClass(); if (!clazz.equals(obj2.getClass())) return false; // 遍历所有字段(含私有字段) Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); Object value1 = field.get(obj1); Object value2 = field.get(obj2); // 处理集合类型 if (value1 instanceof Collection && value2 instanceof Collection) { Collection<?> col1 = (Collection<?>) value1; Collection<?> col2 = (Collection<?>) value2; if (col1.size() != col2.size()) return false; Iterator<?> it1 = col1.iterator(); Iterator<?> it2 = col2.iterator(); while (it1.hasNext()) { if (!deepEquals(it1.next(), it2.next())) return false; } } // 处理数组类型 else if (value1 != null && value1.getClass().isArray() && value2 != null && value2.getClass().isArray()) { int length = java.lang.reflect.Array.getLength(value1); if (java.lang.reflect.Array.getLength(value2) != length) return false; for (int i = 0; i < length; i++) { if (!deepEquals(java.lang.reflect.Array.get(value1, i), java.lang.reflect.Array.get(value2, i))) return false; } } // 处理嵌套对象(非基础类型/包装类) else if (value1 != null && !isPrimitiveOrWrapper(value1.getClass())) { if (!deepEquals(value1, value2)) return false; } // 基础类型与包装类直接对比 else if (!Objects.equals(value1, value2)) { return false; } } return true; } // 判断是否是基础类型或其包装类 private static boolean isPrimitiveOrWrapper(Class<?> clazz) { return clazz.isPrimitive() || clazz.equals(Integer.class) || clazz.equals(Long.class) || clazz.equals(Boolean.class) || clazz.equals(Double.class) || clazz.equals(Float.class) || clazz.equals(Character.class) || clazz.equals(Byte.class) || clazz.equals(Short.class); } }
测试时直接调用工具方法断言:
import static org.junit.jupiter.api.Assertions.assertTrue; // 测试代码示例 assertTrue(ObjectComparator.deepEquals(cloned, original));
3. 额外防护:静态代码检查工具
除了测试层面的保障,还可以用静态代码分析工具(如SonarQube)配置自定义规则,当类新增字段但拷贝构造函数未同步处理时,自动触发警告。也可以在项目中编写自定义Lint规则,强制拷贝构造函数必须覆盖所有字段,从源头减少遗漏风险。
内容的提问来源于stack exchange,提问作者Florian
相关产品推荐
相关产品推荐

