Consider extending the assertions API for arrays and iterables with satisfy* methods accepting multiple Consumer<? super ELEMENT>s. The consumers probe the individual elements of the tested collection.
The additional methods could be:
satisfy(Consumer<? super ELEMENT>... consumers): For each element of the tested collection the (remaining) consumers are called; if a consumer accepts the element, then the element and that consumer are removed from their collections and the next element is probed.satisfyExactly(Consumer<? super ELEMENT>... consumers): The tested collection must have the same size as the consumers. Each element of the tested collection is fed to the corresponding consumer.satisfyExactlyInAnyOrder(Consumer<? super ELEMENT>... consumers): The tested collection must have the same size as the consumers. For each element of the tested collection the (remaining) consumers are called; if a consumer accepts the element, then the element and that consumer are removed from their collections and the next element is probed.This would allow more flexible assertions on arrays and iterables.
assertThat(trilogy).satisfy(movie -> {
// the two Towers
assertThat(movie.getReleaseDate()).isEqualTo(parse("2002-12-18"));
assertThat(movie.getTitle()).contains("Towers");
},
movie -> {
// the fellowship of the Ring
assertThat(movie.getReleaseDate()).isBefore(parse("2002-12-17"));
assertThat(movie.xrated).isFalse();
}
);
assertThat(trilogy).satisfyExactly(movie -> {
// the fellowship of the Ring
assertThat(movie.getReleaseDate()).isBefore(parse("2002-12-17"));
assertThat(movie.xrated).isFalse();
},
movie -> {
// the two Towers
assertThat(movie.getReleaseDate()).isEqualTo(parse("2002-12-18"));
assertThat(movie.getTitle()).contains("Towers");
},
movie -> {
// the Return of the King
assertThat(movie.getTitle()).endsWith("King");
}
);
assertThat(trilogy).satisfyExactlyInAnyOrder(movie -> {
// the two Towers
assertThat(movie.getReleaseDate()).isEqualTo(parse("2002-12-18"));
assertThat(movie.getTitle()).contains("Towers");
},
movie -> {
// the fellowship of the Ring
assertThat(movie.getReleaseDate()).isBefore(parse("2002-12-17"));
assertThat(movie.xrated).isFalse();
},
movie -> {
// the Return of the King
assertThat(movie.getTitle()).endsWith("King");
}
);
The current API provides the following alternatives (for the proposed satisfyExactlyInAnyOrder method):
extracting multiple fields and assert matching tuples:assertThat(trilogy)
.extracting(Movie::getTitle, Movie::getReleaseDate, movie -> movie.xrated)
.containsExactlyInAnyOrder(
tuple("the Return of the King", parse("2003-12-17"), false),
tuple("the fellowship of the Ring", parse("2001-12-19"), false),
tuple("the two Towers", parse("2002-12-18"), false)
);
This approach requires all values to match exactly, requires all elements to be tested for the same fields, is not type safe, and gets unreadable above three fields.
assertThat for each desired element with filterOn distinguishing characteristic of that element: assertThat(trilogy).hasSize(3);
assertThat(trilogy)
.filterOn(movie -> parse("2002-12-17").compareTo(movie.getReleaseDate()) > 0)
.hasSize(1)
.element(0)
.satisfies(movie -> {
assertThat(movie.xrated).isFalse();
});
assertThat(trilogy)
.filterOn("releaseDate", parse("2002-12-18"))
.hasSize(1)
.element(0)
.satisfies(movie -> {
assertThat(movie.getTitle()).contains("Towers");
});
assertThat(trilogy)
.filterOn(movie -> movie.getTitle().endsWith("King"))
.hasSize(1);
While this approach is more flexible than the first, it is more verbose and does usually not reflect the mental model of how a list is tested.
Hey, I really like the idea and would enjoy to tackle that.
@mgrafl @lxndrdnxl I need to look into that and likely split it into different isssues, sorry for the late answer!
@joel-costigliola Would split like to split it into three issues, one per method (satisfy, satisfyExactly, satisfyExactlyInAnyOrder)?
yes that would be good @mgrafl, let's start with satisfy. thanks.
Hello Ting Sun,
Thanks implementing this! I agree with your matching strategy. It is
consistent with my initial description of satisfy(Consumer super
ELEMENT>... consumers): "For each element of the tested collection the
(remaining) consumers are called; if a consumer accepts the element, then
the element and that consumer are removed from their collections and the
next element is probed."
And yes, failure messages are tricky here. Although we could indicate which
elements were not matched, I would also prefer a simple failure message for
now and wait for feedback from users.
Best regards,
Michael
On Fri, May 29, 2020 at 4:14 PM Ting Sun notifications@github.com wrote:
When I try to implement satisfy, one thing I found is very interesting.
Example:Suppose the matching table is as follows:
Consumer 1 Consumer 2 Consumer 3
Element a yes yes yes
Element b yes no no
Element c yes no noThen for every consumer, there is at least one element that can match this
consumer.However, I think the assertion should fail, because an element can only be
used at most one time.The problem is what the failure message should be? As shown in this
example, every consumer has at least one matched element. As far as I am
concerned, we cannot simply say that which consumer is not matched
(especially when the matching table is larger and more complicated) because
it depends on our algorithm.It seems that we cannot show details in the failure message.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/joel-costigliola/assertj-core/issues/1621#issuecomment-635995154,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AEKYEEFLXADJQMO2XPVYK5LRT67NBANCNFSM4I26OZMQ
.
I totally agree with you! And I implemented it in this way. Thanks @mgrafl
@Sunt-ing, your implementation has brought a nuance of satisfy to my attention, that I had not thought of before:
As far as I can see, there are four different strategies:
For each element of the tested collection the (remaining) consumers are called; if a consumer accepts the element, then the element and that consumer are removed from their collections and the next element is probed. (This was my initial proposal.)
Example a (fails):
List<String> starWarsCharacters = newArrayList("Luke", "Leia", "Yoda");
assertThat(starWarsCharacters).satisfy(
name -> {
assertThat(name).contains("L");
}, name -> {
assertThat(name).doesNotContain("a");
});
// "Luke" is probed and accepted by 1st consumer (contains "L").
// --> Element and consumer are removed.
// "Leia" is probed and none of the remaining consumers (i.e., 2nd consumer) accept.
// --> Failure
Example b (also fails):
List<String> starWarsCharacters = newArrayList("Luke", "Leia", "Yoda");
assertThat(starWarsCharacters).satisfy( // order of consumers changed
name -> {
assertThat(name).doesNotContain("a");
}, name -> {
assertThat(name).contains("L");
});
This strategy depends on the ordering of elements to be tested.
Example c (succeeds);
List<String> starWarsCharacters = newArrayList("Leia", "Luke", "Yoda"); // order of elements changed
assertThat(starWarsCharacters).satisfy(
name -> {
assertThat(name).contains("L");
}, name -> {
assertThat(name).doesNotContain("a");
});
// "Leia" is probed and accepted by 1st consumer (contains "L").
// --> Element and consumer are removed.
// "Luke" is probed and accepted by 2nd consumer (does not contain "a").
// --> Success
This is probably not what programmers would expect, since they usually have no control over the order of elements at this point.
For each consumer of the tested collection the (remaining) elements are probed; if an element is accepted by the consumer, then that element and the consumer are removed from their collections and the next consumer is probed.
Example a (fails):
List<String> starWarsCharacters = newArrayList("Luke", "Leia", "Yoda");
assertThat(starWarsCharacters).satisfy(
name -> {
assertThat(name).contains("L"); // "Luke" matches and is removed from elements
}, name -> {
assertThat(name).doesNotContain("a"); // Fails because "Luke" was removed
});
Example b (succeeds):
List<String> starWarsCharacters = newArrayList("Luke", "Leia", "Yoda");
assertThat(starWarsCharacters).satisfy( // order of consumers changed
name -> {
assertThat(name).doesNotContain("a"); // "Luke" matches and is removed from elements
}, name -> {
assertThat(name).contains("L"); // "Leia" matches
});
However, this also depends on the order of elements.
Example c (succeeds):
List<String> starWarsCharacters = newArrayList("Leia", "Luke", "Yoda"); // order of elements changed
assertThat(starWarsCharacters).satisfy(
name -> {
assertThat(name).contains("L"); // "Leia" matches and is removed from elements
}, name -> {
assertThat(name).doesNotContain("a"); // "Luke" matches
});
Verifies that the all the given consumers can be satisfied by elements in the iterable with an element at most satisfies one consumer. No order requirement. (This is the implementation proposed by @Sunt-ing in #1892.)
In other words, it checks whether there is any re-ordering of elements to be tested so that each consumer accepts the corresponding element.
Above examples a,b & c all succeed with this strategy.
Example d (fails):
List<String> starWarsCharacters = newArrayList("Luke", "Leia", "Yoda");
assertThat(starWarsCharacters).satisfy(
name -> {
assertThat(name).contains("k"); // "Luke" matches
}, name -> {
assertThat(name).contains("u"); // "Luke" already matched by first consumer
});
This strategy does not depend on the order elements.
This approach has the highest computational complexity (each consumer is probed with each element and a mutual exclusion of satisfying elements must be checked). But even though the number of elements could be huge, the number of consumers will usually be quite small. So this should not be a problem.
For each consumer check whether at least one element in the iterable satisfies the consumer. One element can satisfy multiple consumers.
Above examples a,b & c all succeed with this strategy.
Example d (succeeds):
List<String> starWarsCharacters = newArrayList("Luke", "Leia", "Yoda");
assertThat(starWarsCharacters).satisfy(
name -> {
assertThat(name).contains("k"); // "Luke" matches
}, name -> {
assertThat(name).contains("u"); // "Luke" matches
});
This strategy does not depend on the order elements.
This strategy would also allow for more consumers than elements to be tested.
Example e (succeeds):
List<String> starWarsCharacters = newArrayList("Luke");
assertThat(starWarsCharacters).satisfy( // more consumers than elements
name -> {
assertThat(name).contains("k"); // "Luke" matches
}, name -> {
assertThat(name).contains("u"); // "Luke" matches
});
I currently prefer strategy 3 (consumers match distinct elements), but I would appreciate additional opinions @joel-costigliola.
In any case, the method's documentation should clearly explain the chosen approach.
@mgrafl Yes, it needs to be discussed. We need to choose useful ones from all the four strategies and pick appropriate API names for them. Then clarify them in the doc.
I have given it some more thought and I am convinced that the currently implemented strategy 3 (consumers match distinct elements) is most appropriate because it has the least unexpected surprises and it will easily fit with satisfyExactlyInAnyOrder().
As this assertion is named I would expect all given consumers to be satisfied but it would not matter if multiple elements satisfy the same consumer (I think this is option 4.).
An extreme case of this would be that one element to satisfy all consumers (basically would be like anyMatch).
List<String> starWarsCharacters = newArrayList("Luke", "Leia", "Yoda");
// assertion passes as both consumer are met, the first one by two elements.
assertThat(starWarsCharacters).satisfy(name -> assertThat(name).contains("L"),
name -> assertThat(name).contains("Y"));
// assertion passes as "Luke" satisfies all
assertThat(starWarsCharacters).satisfy(name -> assertThat(name).contains("L"),
name -> assertThat(name).contains("u"),
name -> assertThat(name).contains("k"),
name -> assertThat(name).contains("e"));
If we want to go for option 3 (one element at most satisfies a consumer), we need to find a non ambiguous name.
@mgrafl @Sunt-ing, thoughts?
My initial idea was to reuse the naming pattern from contains, containsExactly, and containsExactlyInAnyOrder.
For satisfyExactly and satisfyExactlyInAnyOrder, the names already suggest a one-to-one correlation between elements and consumers.
But I agree that the method name satisfy is ambiguous.
On option 3: I think the current description is misleading and does not reflect to proposed implementation. It is not about preventing multiple elements from satisfying the same consumer.
I propose to correct the description: _"Verifies that all given consumers can be satisfied by separate elements in the iterable under test. No order requirement."_
How about subCollectionSatisfyInAnyOrder (the terms "subSet" and "subList" would also be possible but would suggest specific Java types)?
Or someElementsSatisfyInAnyOrder (someElementsIndividuallySatisfyInAnyOrder or someElementsSeparatelySatisfyInAnyOrder is a bit long for my taste)?
List<String> starWarsCharacters = newArrayList("Luke", "Leia", "Yoda");
// assertion passes as both consumer are met, the first one by two elements.
assertThat(starWarsCharacters).someElementsSatisfyInAnyOrder(
name -> assertThat(name).contains("L"),
name -> assertThat(name).contains("Y"));
// assertion fails because "Leia" satisfies "L", "Luke" satisfies "u", but no separate element satisfies "k"
assertThat(starWarsCharacters).someElementsSatisfyInAnyOrder(
name -> assertThat(name).contains("L"),
name -> assertThat(name).contains("u"),
name -> assertThat(name).contains("k"),
name -> assertThat(name).contains("e"));
if we make the parallel between satisfy and contains then my interpretation is correct, after all
contains is like satisfy + isEqualTo:
assertThat(starWarsCharacters).contains("Yoda", "Luke");
vs
assertThat(starWarsCharacters).satisfy(name -> assertThat(name).isEqualTo("L"),
name -> assertThat(name).isEqualTo("Y"));
I propose to correct the description: "Verifies that all given consumers can be satisfied by separate elements in the iterable under test. No order requirement."
satisfyByDifferentElements is the best that I can find but it still sounds a bit awkward:
assertThat(starWarsCharacters).satisfyByDifferentElements(name -> assertThat(name).isEqualTo("L"),
name -> assertThat(name).isEqualTo("Y"));
Unless we find a crystal clear name, I'm not too keen to add this assertion, I'm fine with satisfy, satisfyExactly, satisfyExactlyInAnyOrder although this does not address the previous assertion.
How about containsElementsSatisfying?
assertThat(starWarsCharacters).containsElementsSatisfying(
name -> assertThat(name).contains("L"),
name -> assertThat(name).contains("Y"));
(Side node: I do not fully understand the consumers in your previous example isEqualTo(name).contains("L") and assertThat(name).isEqualTo("Y").)
Sorry bad copy paste, example updated.
containsElementsSatisfying is a clear name but it does not convey the fact that only a consumers are matched by different elements if that was your intention
containsElementsSatisfyingExactlyInAnyOrder, or is that too long?
A minor variation would be containsElementsThatSatisfyExactlyInAnyOrder (though I slightly prefer the containsElementsSatisfyingExactlyInAnyOrder variant).
Although it is a bit long, it conveys the intention of the option 3 (implemented by #1892).
If somebody finds a better (and shorter) name in the future, an alias method name can be added easily.
It would be great if this functions could ( or had sibling functions that did ) operate not on the collection ELEMENT but on a assertion over that element. It could be achieved by requiring an extra parameter which would be the assert factory. The signature could be something like:
public <A extends AbstractAssert<?,ELEMENT>>
ListAssert<SELF,ACTUAL,ELEMENT,ELEMENT_ASSERT> satisfiesExactly(
Function<ELEMENT,A> assertFactory,
Consumer<A>... consumers
)
Such a function would allow to avoid the recurring 'assertThat' such as:
assertThat(starWarsCharacters).someElementsSatisfyInAnyOrder(
name -> assertThat(name).contains("L"),
name -> assertThat(name).contains("u"),
name -> assertThat(name).contains("k"),
name -> assertThat(name).contains("e"));
allowing:
assertThat(starWarsCharacters).someElementsSatisfyInAnyOrder(
Assertions::assertThat,
name -> name.contains("L"),
name -> name.contains("u"),
name -> name.contains("k"),
name -> name.contains("e"));
Unfortunately this functions could not be overloads of the originally suggested ones, as it would make using them ambiguous to the compiler
Hey - was really wanting this assertion and when searching for existing issues before filing one, found this. Any way to get consensus on the naming? FWIW, only the exact versions are interesting to me, satisfyExactly and satisfyExactlyInAnyOrder, and perhaps they aren't as ambiguous? Even just satisfyExactly would work great.
@anuraaga, thanks for reviving the discussion.
If you have any naming suggestions for satisfy (aka. containsElementsSatisfyingExactlyInAnyOrder), please share them.
I agree that the other two methods satisfyExactly and satisfyExactlyInAnyOrder are less controversial and could be implemented right away.
For satisfyExactly, I expect the implementation to be straight forward.
For satisfyExactlyInAnyOrder, the implementation from #1892 could be combined with a length verification.
I will create separate issues for these two methods.
@jbytecoder, I like your idea of having an optional parameter for an assert factory. It makes the code easier to read if you have many consumers.
From a quick prototype, I don't expect any overloading problems if both method variants are introduced simultaneously. The most common usage of parameters will be the one from your example: first the method reference to Assertions::assertThat, followed by lambda expressions for the consumers. This is unambiguous and the compiler handles it well.
Only if the first parameter implements both Function and Consumer, the caller will have to cast it accordingly.
One tiny technical remark: I would specify the method signature as
public final <A extends AbstractAssert<?, ELEMENT>>
SELF satisfyExactly(Function<ELEMENT, A> assertFactory, Consumer<? super A>... consumers)
This would also support consumers of a super-type of A. For example:
// extracted variables also used in other assertions:
Consumer<AbstractCharSequenceAssert<?, ?>> consumer1 = AbstractCharSequenceAssert::isNotBlank;
Consumer<AbstractStringAssert<?>> consumer2 = s -> s.isGreaterThan("a");
assertThat(elements).satisfyExactly(
Assertions::assertThat,
consumer1,
consumer2);
If it's okay for @joel-costigliola, I will include your method variant in the new issues for satisfyExactly and satisfyExactlyInAnyOrder.
Most helpful comment
containsElementsSatisfyingExactlyInAnyOrder, or is that too long?