Asserting properties on list elements with assertJ
Asked Answered
L

7

13

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).

Lucero answered 31/12, 2017 at 12:51 Comment(2)
Is that your real usecase, or would you rather like to check, for example, that, let's say, a list of users has users with the name "John" and "Jack" and with the age 25 and 45?Hordein
The real use case is a source code generator. Each entry of the list is one generated source code snippet. The source code is not parsed (only written to the disk), but there are scenarios where the source code should or should not contain certain patterns. A solution for decomposing objects would not solve the problem.Lucero
K
17

AssertJ v3.19.0 or newer: use satisfiesExactly.

AssertJ v3.19.0, released in 2021, has added a satisfiesExactly method.

So you can write:

assertThat(mylist)
    .satisfiesExactly(item1 -> assertThat(item1).contains("15"),
                      item2 -> assertThat(item2).contains("217"));

You can add more assertions to individual elements if need be:

assertThat(mylist)
    .satisfiesExactly(item1 -> assertThat(item1)
                                      .contains("08")
                                      .contains("15"),
                      item2 -> assertThat(item2).contains("217"));

In comparison to the technique that uses a next() chain, this one also checks the list size for you. As an added benefit, it lets you use whatever lambda parameter you like, so it’s easier to read and to keep track of which element you’re in.

Koziol answered 13/7, 2022 at 20:1 Comment(0)
N
9

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

Nevins answered 1/1, 2018 at 4:2 Comment(3)
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.Lucero
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.Lucero
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.Nevins
F
3

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.
Flirt answered 25/8, 2020 at 9:52 Comment(0)
H
1

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.

Hordein answered 31/12, 2017 at 16:2 Comment(1)
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.Lucero
G
1

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"]>
Gravitative answered 7/2, 2020 at 13:12 Comment(0)
L
0

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.

Lucillelucina answered 31/12, 2017 at 16:10 Comment(3)
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.Lucero
I will wait for more answers, but indeed hamcrest seems to be more flexible in this scenario.Lucero
@Lucero 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.Lucillelucina
H
0

Below is my example using assertJ assertions.

@EqualsAndHashCode
@AllArgsConstructor 
@Getter
class Employee {

private String firstName;
private String lastName;

private List<Project> projects;
}

Project Class

 @EqualsAndHashCode
 @AllArgsConstructor
 @Getter
    class Project {
    private String id;
    private String name;
    }

And we want to test the below object.

var project1 = new Project("1","Customer Management");
var project2 = new Project("1","Employee Management");
var project3 = new Project("1","School Management");

var employees = List.of(
    new Employee("sanjay","bharwani",List.of(project1,project2)),
    new Employee("Sanjay Kumar","Bharwani",List.of(project13))
);

Now below is the assertion for List of employees along with nested projects assertions. All using assertJ framework.

assertThat(employees).satisfiesExactly(
    e1 -> assertThat(e1).hasFieldOrPropertyWithValue("firstName", "sanjay")
    .hasFieldOrPropertyWithValue("lastName", "Bharwani")
    .hasFieldOrPropertyWithValue("projects", List.of(project1, project2)),

    e2 -> assertThat(e2).hasFieldOrPropertyWithValue("firstName", "Sanjay Kumar")
    .hasFieldOrPropertyWithValue("lastName", "Bharwani")
    .hasFieldOrPropertyWithValue("projects", List.of(project3))
 }
Harald answered 24/3, 2023 at 11:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.