Asserting properties on list elements with assertJ

22,357

Solution 1

For this kind of assertions Hamcrest is superior to AssertJ, you can mimic Hamcrest with Conditions but you need to write them as there are none provided out of the box in AssertJ (assertJ philosphy is not to compete with Hamcrest on this aspect).

In the next AssertJ version (soon to be released!), you will be able to reuse Hamcrest Matcher to build AssertJ conditions, example:

Condition<String> containing123 = new HamcrestCondition<>(containsString("123"));

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

As a final note, this suggestion ...

assertThat(mylist).elements()
                  .next().contains("15")
                  .next().contains("217")

... unfortunately can't work because of generics limitation, although you know that you have a List of String, Java generics are not powerful enough to choose a specific type (StringAssert) depending on another (String), this means you can only perform Object assertion on the elements but not String assertion.

-- edit --

Since 3.13.0 one can use asInstanceOf to get specific type assertions, this is useful if the declared type is Object but the runtime type is more specific.

Example:

// Given a String declared as an Object
Object value = "Once upon a time in the west";

// With asInstanceOf, we switch to specific String assertion by specifying the InstanceOfAssertFactory for String
assertThat(value).asInstanceOf(InstanceOfAssertFactories.STRING)
                 .startsWith("Once");`

see https://assertj.github.io/doc/#assertj-core-3.13.0-asInstanceOf

Solution 2

You can use anyMatch

assertThat(mylist)
  .anyMatch(item -> item.contains("15")
  .anyMatch(item -> item.contains("217")

but unfortunately the failure message cannot tell you internals about the expectations

Expecting any elements of:
  <["Abcd15", "218"]>
to match given predicate but none did.

Solution 3

The closest I've found is to write a "ContainsSubstring" condition, and a static method to create one, and use

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

But maybe you should simply write a loop:

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));
}

Or to write a parameterized test, so that each element is tested on each substring by JUnit itself.

Solution 4

You can do the following:

List<String> list1 = Arrays.asList("Abcd15", "217aB");
List<String> list2 = Arrays.asList("Abcd15", "218");

Comparator<String> containingSubstring = (o1, o2) -> o1.contains(o2) ? 0 : 1;
assertThat(list1).usingElementComparator(containingSubstring).contains("15", "217");  // passes
assertThat(list2).usingElementComparator(containingSubstring).contains("15", "217");  // fails

The error it gives is:

java.lang.AssertionError: 
Expecting:
 <["Abcd15", "218"]>
to contain:
 <["15", "217"]>
but could not find:
 <["217"]>

Solution 5

In fact, you must implements your own Condition in assertj for checking the collection containing the substrings in order. for example:

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

What's approach did I choose to meet your requirements? write a contract test case, and then implements the feature that the assertj doesn't given, here is my test case for the hamcrest contains(containsString(...)) adapt to assertj containsExactly as below:

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);
    }
}

However, you down to use chained Conditions rather than the assertj fluent api, so I suggest you to try use the hamcrest instead. in other words, if you use this style in assertj you must write many Conditions or adapt hamcrest Matchers to assertj Condition.

Share:
22,357
CoronA
Author by

CoronA

Updated on July 21, 2022

Comments

  • CoronA
    CoronA almost 2 years

    I have a working hamcrest assertion:

    assertThat(mylist, contains(
      containsString("15"), 
      containsString("217")));
    

    The intended behavior is:

    • mylist == asList("Abcd15", "217aB") => success
    • myList == asList("Abcd15", "218") => failure

    How can I migrate this expression to assertJ. Of course there exist naive solutions, like asserting on the first and second value, like this:

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

    But these are assertions on the list elements, not on the list. Trying asserts on the list restricts me to very generic functions. So maybe it could be only resolved with a custom assertion, something like the following would be fine:

    assertThat(mylist).elements()
      .next().contains("15")
      .next().contains("217")
    

    But before I write a custom assert, I would be interested in how others would solve this problem?

    Edit: One additional non-functional requirement is, that the test should be easily extendible by additional contstraints. In Hamcrest it is quite easy to express additional constraints, e.g.

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

    Tests being dependent on the list index will have to be renumbered for this example, Tests using a custom condition will have to rewrite the complete condition (note that the constraints in allOf are not restricted to substring checks).

  • CoronA
    CoronA over 6 years
    This approach is ok for mere substring checks (as in my example). But the test gets likely unrobust if additional checks are added. I will update my question to point out the robustness criteria.
  • CoronA
    CoronA over 6 years
    The optimium would be that adding a third element at the first position does not imply changing other parts of the program. Besides the naive version (also based on indexes) from my post is more flexible because it allow multiple constraints to one list element.
  • CoronA
    CoronA over 6 years
    I will wait for more answers, but indeed hamcrest seems to be more flexible in this scenario.
  • holi-java
    holi-java over 6 years
    @CoronA yes, if you try to implements such feature in assertj, indeed you must write many Conditions or YourAsserts. beside that your assertj design api elements().next()... doesn't fulfill the hamcrest contains(..) completely. since the hamcrest contains will match both elements in order and 2 collection have the same size.
  • CoronA
    CoronA over 6 years
    My current directions tries to use satisfies instead of Condition, because the Consumer<T> can assert more than one property, which serves the flexibility above.
  • CoronA
    CoronA over 6 years
    Yet I confirm your statement about generics-limitation, but I would suggest to allow breaking this limitation. Is there a reason why assertThat(mylist).first().as(StringAssert.class).contains("‌​15") was not already implemented? This would not help for my scenario (iterating over a list), but it would by quite helpful in other tests of mine.
  • Joel Costigliola
    Joel Costigliola over 6 years
    What is currently implemented is: assertThat(mylist).first().asString().startsWith("prefix");, we thought to add other asXxx methods but that would clutter the API too much. We ended up with this syntax assertThat(list, StringAssert.class).first().startsWith("prefix");. Having said that I might think about your suggestion with as(Assert class) since it is easy to discover, one drawback is that as is already used for describing assertions.