如何使用 Java 记录断言 hasProperty?

问题描述

我在测试中使用了一段代码来检查结果列表是否包含某些属性,使用 Hamcrest 2.2:

assertthat(result.getUsers(),hasItem(
    hasProperty("name",equalTo(user1.getName()))
));
assertthat(result.getUsers(),equalTo(user2.getName()))
));

NameDto一个普通类时,这工作得很好。但是在我将其更改为 Record 后,Hamcrest 的 hasProperty 抱怨没有名为 name属性

java.lang.AssertionError:
Expected: a collection containing hasProperty("name","Test Name")
     but: mismatches were: [No property "name",No property "name"]

是否有其他匹配器可以用来实现与以前相同的匹配?或者我可以使用其他一些解决方法来让它处理记录?

解决方法

记录字段的访问器方法不遵循常规的 JavaBeans 约定,因此 User 记录(例如 public record User (String name) {})将具有名称为 name() 而不是 { {1}}。

我怀疑这就是 Hamcrest 认为没有财产的原因。除了编写自定义 Matcher 之外,我认为 Hamcrest 没有开箱即用的方法。

这是一个受现有 HasPropertyWithValue 启发的自定义 getName()。这里利用的主要实用程序是 Java 的 Class.getRecordComponents():

HasRecordComponentWithValue
,

我发现使用 AssertJ就可以实现相同的测试,至少在这种情况下:

assertThat(result.getUsers())
        .extracting(UserDto::name)
        .contains(user1.getName(),user2.getName());

它没有使用 hasProperty,所以它并不能完全解决问题。

,

Hamcrest 实际上遵循 JavaBeans 标准(允许任意访问器名称),因此我们可以使用 hasProperty 来做到这一点。如果你想。不过,我不确定您是否会这样做 - 这很麻烦。

如果我们遵循 the source for HasPropertyWithValue 的工作原理,我们会发现它通过在相关类的 PropertyDescriptor 中查找属性的 BeanInfo 来发现访问器方法的名称,通过检索java.beans.Introspector

Introspector 有一些关于如何解析给定类的 BeanInfo 的非常有用的文档:

Introspector 类提供了学习工具的标准方法 关于目标 Java 支持的属性、事件和方法 豆。

对于这三种信息中的每一种,Introspector 都会 分别分析bean的类和超类寻找 显式或隐式信息,并使用该信息来 构建一个全面描述目标的 BeanInfo 对象 豆。

对于每个类“Foo”,如果有明确的信息,则可能可用 存在一个相应的“FooBeanInfo”类,它提供了一个非空的 查询信息时的值。我们首先查找BeanInfo 通过获取目标 bean 的完整包限定名称来进行类 class 并附加“BeanInfo”以形成新的类名。如果这 失败,然后我们取这个名字的最后一个类名组件,和 在 BeanInfo 中指定的每个包中查找该类 包搜索路径。

因此对于诸如“sun.xyz.OurButton”之类的类,我们将首先寻找一个 BeanInfo 类称为“sun.xyz.OurButtonBeanInfo”,如果失败 我们会在 BeanInfo 搜索路径中的每个包中查找 我们的ButtonBeanInfo 类。使用默认搜索路径,这意味着 寻找“sun.beans.infos.OurButtonBeanInfo”。

如果一个类提供了关于它自己的显式 BeanInfo,那么我们将它添加到 我们从分析任何派生数据中获得的 BeanInfo 信息 类,但我们认为明确的信息是确定的 对于当前类及其基类,不要进行任何操作 进一步向上超类链。

如果我们没有在一个类上找到显式的 BeanInfo,我们就使用低级 反思学习班级方法并应用标准设计 识别属性访问器、事件源或公共的模式 方法。然后我们继续分析类的超类并添加 来自它的信息(可能在超类链上)。

您可能认为 Introspector 可以在最后一步(“我们使用低级反射”)中找到记录并生成正确的 BeanInfo,但它似乎不是。如果你用谷歌搜索一下,你会在 JDK 开发列表上找到一些关于添加这个的讨论,但似乎什么也没发生。可能是 JavaBeans 规范必须更新,我想这可能需要一些时间。

但是,为了回答您的问题,我们所要做的就是为您拥有的每一种记录类型提供一个 BeanInfo。然而,手写它们并不是我们想要做的事情 - 这甚至比使用 getter 和 setter(以及 equalshashCode 等等)编写类的老式方法更糟糕。

我们可以自动生成 bean 信息作为构建步骤(或在我们启动应用程序时动态生成)。一种更简单的方法(需要一些样板文件)是制作可用于所有记录类的通用 BeanInfo。这是一个最小努力的方法。首先,假设我们有这个记录:

public record Point(int x,int y){}

还有一个把它当作一个bean的主类:

public class Main {
    public static void main(String[] args) throws Exception {
        var bi = java.beans.Introspector.getBeanInfo(Point.class);
        var bean = new Point(4,2);
        for (var prop : args) {
            Object value = Stream.of(bi.getPropertyDescriptors())
                .filter(pd -> pd.getName().equals(prop))
                .findAny()
                .map(pd -> {
                    try {
                        return pd.getReadMethod().invoke(bean);
                    } catch (ReflectiveOperationException e) {
                        return "Error: " + e;
                    }
                })
                .orElse("(No property with that name)");
            System.out.printf("Prop %s: %s%n",prop,value);
        }
    }
}

如果我们像 java Main x y z 那样编译和运行,你会得到这样的输出:

Prop x: (No property with that name)
Prop y: (No property with that name)
Prop z: (No property with that name)

所以它没有像预期的那样找到记录组件。让我们制作一个通用的 BeanInfo

public abstract class RecordBeanInfo extends java.beans.SimpleBeanInfo {

    private final PropertyDescriptor[] propertyDescriptors;

    public RecordBeanInfo(Class<?> recordClass) throws IntrospectionException {
        if (!recordClass.isRecord())
            throw new IllegalArgumentException("Not a record: " + recordClass);
        var components = recordClass.getRecordComponents();
        propertyDescriptors = new PropertyDescriptor[components.length];
        for (var i = 0; i < components.length; i++) {
            var c = components[i];
            propertyDescriptors[i] = new PropertyDescriptor(c.getName(),c.getAccessor(),null);
        }
    }

    @Override
    public PropertyDescriptor[] getPropertyDescriptors() {
        return this.propertyDescriptors.clone();
    }

}

在我们的工具箱中有这个类,我们所要做的就是用一个正确名称的类来扩展它。对于我们的示例,PointBeanInfoPoint 记录位于同一包中:


public class PointBeanInfo extends RecordBeanInfo {
    public PointBeanInfo() throws IntrospectionException {
        super(Point.class);
    }
}

有了所有这些东西,我们运行我们的主类并得到预期的输出:

$ java Main x y z
Prop x: 4
Prop y: 2
Prop z: (No property with that name)

结束语:如果您只想使用属性使单元测试看起来更好,我建议使用其他答案中给出的解决方法之一,而不是我提出的过度设计的方法。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...