多测试场景下POJO校验方案:动态忽略空值与默认值并输出差异
解决方案:动态POJO校验(忽略空值/默认值+输出差异)
针对静态注解无法适配多测试场景、equals方法无法忽略空值/默认值且无差异输出的问题,提供两种实用方案:
方案一:使用AssertJ递归比较(推荐,无需自定义代码)
AssertJ的RecursiveComparison支持动态忽略字段、空值、默认值,且会自动输出详细差异,完美适配多测试场景。
测试类改造示例
import static org.assertj.core.api.Assertions.assertThat; @Test public void shouldBeAbleToCreateLead() throws Exception { String query = "Select * from growth.merchant_lead where email = 'pulkit.agrawal+testingtoday@clover.com' order by created_time desc"; Lead requestCreateLead = leadBuilder(); Response response = LeadAPI.put(requestCreateLead); assertThat(response.statusCode()).isEqualTo(StatusCode.CODE_200.getCode()); LeadResponse actualResponseLead = response.as(LeadResponse.class); JSONArray jsonArray = getDataRows(query); LeadResponse expectedResponse = getExpectedResponse(jsonArray); // 核心校验逻辑 assertThat(actualResponseLead) .usingRecursiveComparison() .ignoringFieldsWithNullValues() // 忽略空值字段 .ignoringFieldsWithDefaultValues() // 忽略默认值字段(如Integer=0、Boolean=false) // 针对特定测试用例,可动态添加需要忽略的字段 // .ignoringFields("hasSubscriptions", "planId") .isEqualTo(expectedResponse); }
优势
- 无需自定义工具类,开箱即用
- 差异输出清晰,直接定位不一致的字段路径和值
- 支持动态调整忽略规则,不同测试用例可灵活配置
方案二:自定义反射校验工具类(完全自定义规则)
如果需要更精细的控制(比如自定义默认值判断逻辑),可以自己实现基于反射的校验函数,同时记录差异信息。
自定义POJO比较器
import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Objects; public class POJOComparator { private final List<String> differences = new ArrayList<>(); // 对外暴露的比较方法,返回是否相等 public boolean compare(Object actual, Object expected) { differences.clear(); if (actual == null && expected == null) return true; if (actual == null || expected == null) { differences.add("对象非空性不一致:actual=" + actual + ", expected=" + expected); return false; } if (!actual.getClass().equals(expected.getClass())) { differences.add("类类型不匹配:actual=" + actual.getClass().getName() + ", expected=" + expected.getClass().getName()); return false; } compareFields(actual, expected, actual.getClass(), ""); return differences.isEmpty(); } // 递归比较字段(含父类字段) private void compareFields(Object actual, Object expected, Class<?> clazz, String prefix) { if (clazz == Object.class) return; for (Field field : clazz.getDeclaredFields()) { field.setAccessible(true); String fieldPath = prefix.isEmpty() ? field.getName() : prefix + "." + field.getName(); try { Object actualValue = field.get(actual); Object expectedValue = field.get(expected); // 忽略双方都为空的字段 if (actualValue == null && expectedValue == null) continue; // 忽略任意一方为默认值的字段 if (isDefaultValue(actualValue) || isDefaultValue(expectedValue)) continue; // 复杂对象(自定义POJO、集合)递归比较 POJOComparator subComparator = new POJOComparator(); if (!subComparator.compare(actualValue, expectedValue)) { differences.addAll(subComparator.getDifferences().stream().map(d -> fieldPath + "." + d).toList()); } } catch (IllegalAccessException e) { differences.add(fieldPath + ": 字段访问异常 - " + e.getMessage()); } } // 递归处理父类字段 compareFields(actual, expected, clazz.getSuperclass(), prefix); } // 判断是否为默认值 private boolean isDefaultValue(Object value) { if (value == null) return false; Class<?> type = value.getClass(); return switch (type.getSimpleName()) { case "Integer" -> (Integer) value == 0; case "Long" -> (Long) value == 0L; case "Boolean" -> !((Boolean) value); case "String" -> ((String) value).isEmpty(); case "int", "long", "boolean" -> value.toString().matches("0|false"); default -> false; }; } // 判断是否为需要递归比较的复杂类型 private boolean isComplexType(Class<?> type) { return !type.isPrimitive() && !type.isEnum() && !isWrapperType(type) && !type.equals(String.class) && (type.isArray() || Iterable.class.isAssignableFrom(type) || type.getPackageName().startsWith("com.spotify.oauth2.pojo")); } // 判断是否为基本类型包装类 private boolean isWrapperType(Class<?> type) { return type == Integer.class || type == Long.class || type == Boolean.class || type == Double.class || type == Float.class || type == Short.class || type == Byte.class || type == Character.class; } // 获取差异列表 public List<String> getDifferences() { return new ArrayList<>(differences); } }
测试类中使用
@Test public void shouldBeAbleToCreateLead() throws Exception { // 原有逻辑不变... LeadResponse actualResponseLead = response.as(LeadResponse.class); LeadResponse expectedResponse = getExpectedResponse(jsonArray); POJOComparator comparator = new POJOComparator(); boolean isEqual = comparator.compare(actualResponseLead, expectedResponse); // 输出差异并断言 if (!isEqual) { System.err.println("POJO校验失败,差异如下:"); comparator.getDifferences().forEach(System.err::println); // 抛出断言失败异常,终止测试 assertThat(isEqual).withFailMessage(String.join("\n", comparator.getDifferences())).isTrue(); } }
优势
- 完全自定义忽略规则和差异输出格式
- 可以根据业务需求扩展逻辑(比如忽略特定规则的字段)
内容的提问来源于stack exchange,提问作者Pulkit Agrawal
相关产品推荐
相关产品推荐

