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

多测试场景下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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 19:44:56