将对象列表与属性列表进行匹配

Match List of objects against list of properties

我正在尝试使用 hamcrest 匹配器将对象列表与其属性的 list/array 进行匹配。对于一个 属性 值,这不是问题,因为我可以这样做:

assertThat(savedGroup.getMembers(),
    containsInAnyOrder(hasProperty("name", is(NAMES[0]))));

对于多个 属性 值,我可以使用多个 hasProperty() 调用

assertThat(savedGroup.getMembers(),
    containsInAnyOrder(
        hasProperty("name", is(NAMES[0])),
        hasProperty("name", is(NAMES[1]))));

但是有没有通用的方法来匹配 NAMES 数组中的所有值?

containsInAnyOrder 的一个重载接受一组匹配器作为其参数。因此你可以这样做:

assertThat(
    savedGroup.getMembers(),
    containsInAnyOrder(
        Stream.of(NAMES)
              .map(name -> hasProperty("name", is(name)))
              .collect(Collectors.toList()) 
    ));

(如果使用Java 8,否则需要添加一个循环构建集合)

需要进行一些清理(描述输出),但我认为它确实解决了您的问题:

package org.example.matchers;

import java.util.List;

import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.TypeSafeMatcher;

public class ContainsArrayElementsInAnyOrder<T> extends TypeSafeMatcher<List<T>> {

    private T[] toMatch;

    public ContainsArrayElementsInAnyOrder(final T[] toMatch) {
        this.toMatch = toMatch;
    }

    @Override
    protected boolean matchesSafely(List<T> item) {
        if(item.size() != toMatch.length) {
            return false;
        }
        for (T t : toMatch) {
            if(!item.contains(t)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public void describeMismatchSafely(List<T> item, Description mismatchDescription) {
        mismatchDescription.appendValueList("[", ",", "]", item);
    }

    @Override
    public void describeTo(Description description) {
        description.appendValueList("[", ",", "]", toMatch);
    }

    @Factory
    public static <T> ContainsArrayElementsInAnyOrder<T> containsArrayElementsInAnyOrder(T[] elements) {
        return new ContainsArrayElementsInAnyOrder<T>(elements);
    }    


}

测试:

@Test
public void shouldContainsInAnyOrderSameElementsInArrayAsInList() {
    final String[] NAME = new String[]{"name3", "name1", "name2"};

    final List<String> result = new ArrayList<>(3);
    result.add("name2");
    result.add("name1");
    result.add("name4");

    assertThat(result, containsArrayElementsInAnyOrder(NAME));
}

如果不匹配则输出:

java.lang.AssertionError: 
Expected: ["name3","name1","name2"]
     but: ["name2","name1","name4"]
    at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
    at org.junit.Assert.assertThat(Assert.java:956)
    at org.junit.Assert.assertThat(Assert.java:923)
    at ..

最好的方法 (IMO) 是将重载的 containsInAnyOrder Matcher 与自定义的 FeatureMatcher 结合起来。最终您的代码将如下所示:

String[] expectedNames = new String[] { "John", "Bob", "Carol"};
assertThat(savedGroup.getMembers(), hasNames(expectedNames));

hasNames实现如下:

private Matcher<Iterable<? extends Member>> hasNames(String[] expectedNames) {
    return containsInAnyOrder(Arrays.stream(expectedNames).map(name -> name(name)).collect(Collectors.toList()));
}

最后一部分是对 name 的调用,它生成一个 Matcher,它将以 type-safe 的方式从您的对象中提取 属性:

private Matcher<Member> name(String name) {
    return new FeatureMatcher<Member, String>(equalTo(name), "name", "name") {
        @Override
        protected String featureValueOf(Member actual) {
            return actual.getName();
        }
    };
}

这样做的好处是:

  • 您将受益于 type-safety 而不是使用 hasProperty
  • 您的测试现在描述了您实际想要匹配的内容,即 hasNames
  • 生成的代码现在更加灵活和可组合。想匹配单个对象名称?您现在需要做的就是 assertThat(member, has(name("Fred")))

通过移动 equalTo sub-matcher 成为 hasNames 调用的一部分,您可以获得更多的可组合性,如下所示:

private Matcher<Iterable<? extends Member>> hasNames(String[] expectedNames) {
    return containsInAnyOrder(Arrays.stream(expectedNames).map(name -> name(equalTo(name))).collect(Collectors.toList()));
}

private Matcher<Member> name(Matcher<String> nameMatcher) {
    return new FeatureMatcher<Member, String>(nameMatcher, "name", "name") {
        @Override
        protected String featureValueOf(Member actual) {
            return actual.getName();
        }
    };
}