Asserting properties on list elements with assertJ
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 Condition
s 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 Condition
s or adapt hamcrest Matcher
s to assertj Condition
.
CoronA
Updated on July 21, 2022Comments
-
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")
=> successmyList == 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 over 6 yearsThis 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 over 6 yearsThe 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 over 6 yearsI will wait for more answers, but indeed hamcrest seems to be more flexible in this scenario.
-
holi-java over 6 years@CoronA yes, if you try to implements such feature in assertj, indeed you must write many
Condition
s orYourAssert
s. beside that your assertj design apielements().next()...
doesn't fulfill the hamcrestcontains(..)
completely. since the hamcrestcontains
will match both elements in order and 2 collection have the same size. -
CoronA over 6 yearsMy current directions tries to use
satisfies
instead ofCondition
, because theConsumer<T>
can assert more than one property, which serves the flexibility above. -
CoronA over 6 yearsYet 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 over 6 yearsWhat is currently implemented is:
assertThat(mylist).first().asString().startsWith("prefix");
, we thought to add otherasXxx
methods but that would clutter the API too much. We ended up with this syntaxassertThat(list, StringAssert.class).first().startsWith("prefix");
. Having said that I might think about your suggestion withas(Assert class)
since it is easy to discover, one drawback is thatas
is already used for describing assertions.