关于junit:使用assertJ在列表元素上声明属性

Asserting properties on list elements with assertJ

我有一个可行的hamcrest断言:

1
2
3
assertThat(mylist, contains(
  containsString("15"),
  containsString("217")));

预期的行为是:

  • mylist == asList("Abcd15","217aB") =>成功
  • myList == asList("Abcd15","218") =>失败

如何将这个表达式迁移到assertJ。当然,存在一些天真的解决方案,例如对第一个和第二个值进行断言,如下所示:

1
2
assertThat(mylist.get(0)).contains("15");
assertThat(mylist.get(1)).contains("217");

但是这些是对列表元素的声明,而不是对列表的声明。在列表上尝试断言将我限制在非常通用的功能上。因此,也许只能使用自定义断言来解决它,如下所示就可以了:

1
2
3
assertThat(mylist).elements()
  .next().contains("15")
  .next().contains("217")

但是在编写自定义断言之前,我会对其他人如何解决此问题感兴趣?

编辑:另外一项非功能性要求是,该测试应易于通过其他约束条件进行扩展。在Hamcrest中,表达其他约束非常容易,例如

1
2
3
4
assertThat(mylist, contains(
  emptyString(),                                     //additional element
  allOf(containsString("08"), containsString("15")), //extended constraint
  containsString("217")));                           // unchanged

在此示例中,依赖于列表索引的测试必须重新编号,使用自定义条件的测试将必须重写完整条件(请注意,allOf中的约束不仅限于子字符串检查)。


对于此类断言,Hamcrest优于AssertJ,您可以使用条件来模仿Hamcrest,但由于AssertJ中没有提供任何开箱即用的内容,因此您需要编写它们(assertJ的理念是不与Hamcrest竞争)。

在下一个AssertJ版本(即将发布!)中,您将能够重用Hamcrest Matcher来构建AssertJ条件,例如:

1
2
3
4
5
Condition<String> containing123 = new HamcrestCondition<>(containsString("123"));

// assertions succeed
assertThat("abc123").is(containing123);
assertThat("def456").isNot(containing123);

最后一点,这个建议...

1
2
3
assertThat(mylist).elements()
                  .next().contains("15")
                  .next().contains("217")

......由于泛型的限制,不幸的是,它无法工作,尽管您知道您拥有一个字符串列表,但Java泛型的功能不足以根据另一个(String)选择特定类型(StringAssert),这意味着您只能对元素执行Object声明,而不能对String声明执行。


实际上,您必须在assertj中实现自己的Condition,以按顺序检查包含子字符串的集合。例如:

1
2
3
assertThat(items).has(containsExactly(
  stream(subItems).map(it -> containsSubstring(it)).toArray(Condition[]::new)
));

我选择了哪种方法来满足您的要求?编写合同测试用例,然后实现未提供assertj的功能,这是我针对hamcrest contains(containsString(...))适应assertj containsExactly的测试用例,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import org.assertj.core.api.Assertions;
import org.assertj.core.api.Condition;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import java.util.Collection;
import java.util.List;

import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;

@RunWith(Parameterized.class)
public class MatchersTest {
    private final SubstringExpectation expectation;

    public MatchersTest(SubstringExpectation expectation) {
        this.expectation = expectation;
    }

    @Parameters
    public static List<SubstringExpectation> parameters() {
        return asList(MatchersTest::hamcrest, MatchersTest::assertj);
    }

    private static void assertj(Collection<? extends String> items, String... subItems) {
        Assertions.assertThat(items).has(containsExactly(stream(subItems).map(it -> containsSubstring(it)).toArray(Condition[]::new)));
    }

    private static Condition<String> containsSubstring(String substring) {
        return new Condition<>(s -> s.contains(substring),"contains substring: "%s"", substring);
    }

    @SuppressWarnings("unchecked")
    private static <C extends Condition<? super T>, T extends Iterable<? extends E>, E> C containsExactly(Condition<E>... conditions) {
        return (C) new Condition< T >("contains exactly:" + stream(conditions).map(it -> it.toString()).collect(toList())) {
            @Override
            public boolean matches(T items) {
                int size = 0;
                for (E item : items) {
                    if (!matches(item, size++)) return false;
                }
                return size == conditions.length;
            }

            private boolean matches(E item, int i) {
                return i < conditions.length && conditions[i].matches(item);
            }
        };
    }

    private static void hamcrest(Collection<? extends String> items, String... subItems) {
        assertThat(items, contains(stream(subItems).map(Matchers::containsString).collect(toList())));
    }

    @Test
    public void matchAll() {
        expectation.checking(asList("foo","bar"),"foo","bar");
    }


    @Test
    public void matchAllContainingSubSequence() {
        expectation.checking(asList("foo","bar"),"fo","ba");
    }

    @Test
    public void matchPartlyContainingSubSequence() {
        try {
            expectation.checking(asList("foo","bar"),"fo");
            fail();
        } catch (AssertionError expected) {
            assertThat(expected.getMessage(), containsString(""bar""));
        }
    }

    @Test
    public void matchAgainstWithManySubstrings() {
        try {
            expectation.checking(asList("foo","bar"),"fo","ba","<many>");
            fail();
        } catch (AssertionError expected) {
            assertThat(expected.getMessage(), containsString("<many>"));
        }
    }

    private void fail() {
        throw new IllegalStateException("should failed");
    }

    interface SubstringExpectation {
        void checking(Collection<? extends String> items, String... subItems);
    }
}

但是,您最好使用链接的Condition而不是assertj流利的api,因此建议您尝试使用hamcrest。换句话说,如果在assertj中使用此样式,则必须编写许多Condition或使hamcrest Matcher适应assertj Condition


我发现最接近的是编写一个" ContainsSubstring"条件,以及一个创建该条件的静态方法,并使用

1
2
assertThat(list).has(containsSubstring("15", atIndex(0)))
                .has(containsSubstring("217", atIndex(1)));

但是也许您应该简单地编写一个循环:

1
2
3
4
5
List<String> list = ...;
List<String> expectedSubstrings = Arrays.asList("15","217");
for (int i = 0; i < list.size(); i++) {
    assertThat(list.get(i)).contains(expectedSubstrings.get(i));
}

或编写一个参数化测试,以便JUnit自己在每个子字符串上测试每个元素。