当前位置: 首页 > 知识库问答 >
问题:

如何使用Java记录断言hasProperty?

董喜
2023-03-14

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

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

NameDto是一个普通的类时,这个工作非常好。但是在我将它更改为记录之后,Hamcrest的hasProperty抱怨没有名为name的属性:

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

是否有其他匹配器可以用来实现与之前相同的匹配?或者其他解决方法可以用来让它与记录一起工作?

共有3个答案

卫焕
2023-03-14

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

如果我们遵循HasPropertyWithValue的源代码的工作方式,我们会发现它通过在相关类的BeanInfo中找到属性的PropertyDescriptor来发现访问器方法的名称,该属性是通过java检索的。豆。内省者

对于给定类的BeanInfo是如何解析的,内探器有一些非常有用的留档:

Introspector类为工具提供了一种标准方法,用于了解目标JavaBean支持的属性、事件和方法。

对于这三种信息中的每一种,内省者将分别分析bean的类和超类,寻找显式或隐式信息,并使用这些信息构建全面描述目标bean的BeanInfo对象

对于每个类“Foo”,如果存在相应的“FooBeanInfo”类,在查询信息时提供非空值,则可以使用显式信息。我们首先通过获取目标bean类的完整包限定名并附加“BeanInfo”来寻找BeanInfo类,以形成一个新的类名。如果失败,那么我们将使用该名称的最后一个classname组件,并在BeanInfo包搜索路径中指定的每个包中查找该类。

因此,对于“sun.xyz.OurButton”这样的类,我们首先会查找名为“sun.xyz.ourButtonBeaInfo”的BeanInfo类,如果失败,我们会在BeanInfo搜索路径中的每个包中查找OurButtonBeaInfo类。在默认搜索路径下,这意味着要查找“sun.beans.infos.OurButtonBeanInfo”。

如果一个类提供了关于自身的显式BeanInfo,那么我们将其添加到我们从分析任何派生类中获得的BeanInfo信息中,但我们认为显式信息对于当前类及其基类是确定的,并且不在超类链的任何更上一层。

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

你可能认为内探器可以在最后一步(我们使用低级反射)中查找记录并生成正确的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);
        }
    }
}

如果我们像javamainxyz那样编译和运行,您会得到如下输出:

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)

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

长孙永思
2023-03-14

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

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

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

乌俊健
2023-03-14

记录字段的访问器方法不遵循常规的JavaBeans约定,因此用户记录(比如public record User(String name){})将有一个名为name()的访问器方法,而不是getName()

我怀疑这就是为什么汉克雷斯特认为这里没有房产。我认为在Hamcrest,除了写一个定制的匹配器之外,没有其他方法可以跳出框框。

以下是一个定制的HasRecordComponentWithValue,灵感来自现有的hasprropertywhithvalue。这里使用的主要工具是Java的类。getRecordComponents()

public static class HasRecordComponentWithValue<T> extends TypeSafeDiagnosingMatcher<T> {
    private static final Condition.Step<RecordComponent,Method> WITH_READ_METHOD = withReadMethod();
    private final String componentName;
    private final Matcher<Object> valueMatcher;

    public HasRecordComponentWithValue(String componentName, Matcher<?> valueMatcher) {
        this.componentName = componentName;
        this.valueMatcher = nastyGenericsWorkaround(valueMatcher);
    }

    @Override
    public boolean matchesSafely(T bean, Description mismatch) {
        return recordComponentOn(bean, mismatch)
                  .and(WITH_READ_METHOD)
                  .and(withPropertyValue(bean))
                  .matching(valueMatcher, "record component'" + componentName + "' ");
    }

    private Condition.Step<Method, Object> withPropertyValue(final T bean) {
        return new Condition.Step<Method, Object>() {
            @Override
            public Condition<Object> apply(Method readMethod, Description mismatch) {
                try {
                    return matched(readMethod.invoke(bean, NO_ARGUMENTS), mismatch);
                } catch (Exception e) {
                    mismatch.appendText(e.getMessage());
                    return notMatched();
                }
            }
        };
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("hasRecordComponent(").appendValue(componentName).appendText(", ")
                   .appendDescriptionOf(valueMatcher).appendText(")");
    }

    private Condition<RecordComponent> recordComponentOn(T bean, Description mismatch) {
        RecordComponent[] recordComponents = bean.getClass().getRecordComponents();
        for(RecordComponent comp : recordComponents) {
            if(comp.getName().equals(componentName)) {
                return matched(comp, mismatch);
            }
        }
        mismatch.appendText("No record component \"" + componentName + "\"");
        return notMatched();
    }


    @SuppressWarnings("unchecked")
    private static Matcher<Object> nastyGenericsWorkaround(Matcher<?> valueMatcher) {
        return (Matcher<Object>) valueMatcher;
    }

    private static Condition.Step<RecordComponent,Method> withReadMethod() {
        return new Condition.Step<RecordComponent, java.lang.reflect.Method>() {
            @Override
            public Condition<Method> apply(RecordComponent property, Description mismatch) {
                final Method readMethod = property.getAccessor();
                if (null == readMethod) {
                    mismatch.appendText("record component \"" + property.getName() + "\" is not readable");
                    return notMatched();
                }
                return matched(readMethod, mismatch);
            }
        };
    }

    @Factory
    public static <T> Matcher<T> hasRecordComponent(String componentName, Matcher<?> valueMatcher) {
        return new HasRecordComponentWithValue<T>(componentName, valueMatcher);
    }
}
 类似资料:
  • 我试着 我在调试时获得和是。 如何在使用时断言大于条件

  • 问题内容: 我有一些正在测试的代码,它们呼吁Java记录器报告其状态。在JUnit测试代码中,我想验证是否在此记录器中输入了正确的日志条目。遵循以下内容: 我想可以用专门改编的记录器(或处理程序或格式化程序)来完成此操作,但我希望重用已存在的解决方案。(而且,老实说,我不清楚如何从记录器中获取logRecord,但是假设这是可能的。) 问题答案: 我也需要几次。我在下面整理了一个小样本,你可以根据

  • 我正在使用JUnit和Apache Log4J来学习TDD和日志服务的最佳实践。我有一个类,它有一个方法,它将尝试连接到MySQL数据库并返回类型的对象。 我有一个类GenericTaskInterpreterTests,在这里我为这个方法(和其他方法)编写了测试用例。 在这个测试用例场景中,如何使用TestWatcher记录断言失败

  • 我正在寻找一些方法(最好是通过Java代码),使我能够通过SSO登录从IDP(即ForgeRock的OpenAM)获取服务提供商(SP)的SAML断言。 SP已配置为与IDP交互(例如vCloud Director- 我似乎找不到任何执行SSO登录的方法(通过使用IDP的SAML断言进行身份验证)。到目前为止,我已经看到了十几条参考文献,包括断言(AssertionIDRequestUtil)。来

  • 问题内容: 我想知道为什么关键字在Java中使用不足?我几乎从未见过使用它们,但是我认为它们是个好主意。我当然更喜欢简洁: 冗长的 我怀疑它们没有得到充分利用,因为 他们来得相对较晚(Java 1.4),到那时,许多人已经建立了Java编程风格/习惯 它们默认在运行时关闭 问题答案: __理论上, 断言 是用于测试不变式的假设,这些假设 必须 正确才能使代码正确完成。 所示示例测试了有效输入,这不

  • 我正在尝试使用谷歌办公套件设置SAML。但是我得到了这个错误 我已经配置断言应该在我的SP中签名。我可以在其他IDP(如onelogin、okta)中看到签署响应断言的选项。该配置适用于其他IDP,但看不到在谷歌办公套件SAML中唱响应断言的选项。 在Gsuite中,我只能看到签名响应的选项,而不能看到断言。 如何正确设置此设置? SP元数据如下所示: